├── .github └── workflows │ └── deploy.yaml ├── .gitignore ├── 0000-template.md ├── LICENSE.txt ├── README.md ├── book.toml ├── generate-book.py ├── text ├── 0001-aggregate-data-structures.md ├── 0002-interfaces.md ├── 0003-enumeration-shapes.md ├── 0004-const-castable-exprs.md ├── 0005-remove-const-normalize.md ├── 0006-stdlib-crc.md ├── 0008-aggregate-extensibility.md ├── 0009-const-init-shape-castable.md ├── 0010-move-repl-to-value.md ├── 0015-lifting-shape-castables.md ├── 0016-soc-csr-regs.md ├── 0017-remove-log2-int.md ├── 0018-reorganize-vendor-platforms.md ├── 0019-remove-scheduler.md ├── 0020-deprecate-non-fwft-fifos.md ├── 0021-patch-releases.md ├── 0022-valuecastable-shape.md ├── 0027-simulator-testbenches.md ├── 0028-override-value-operators.md ├── 0030-component-metadata.md ├── 0031-enumeration-type-safety.md ├── 0034-interface-rename.md ├── 0035-shapelike-valuelike.md ├── 0036-async-testbench-functions.md ├── 0037-make-signature-immutable.md ├── 0038-component-signature-immutability.md ├── 0039-empty-case.md ├── 0040-arbitrary-memory-shape.md ├── 0042-const-from-shape-castable.md ├── 0043-rename-reset-to-init.md ├── 0045-lib-memory.md ├── 0046-shape-range-1.md ├── 0049-soc-gpio-peripheral.md ├── 0049-soc-gpio-peripheral │ ├── overview.svg │ ├── reg-input.svg │ ├── reg-mode.svg │ ├── reg-output.svg │ └── reg-setclr.svg ├── 0050-print.md ├── 0051-const-from-bits.md ├── 0052-choice.md ├── 0053-ioport.md ├── 0054-read-port-init.md ├── 0055-lib-io.md ├── 0056-mem-wide.md ├── 0057-single-field-shortcut.md ├── 0058-valuecastable-format.md ├── 0059-no-domain-upwards-propagation.md ├── 0061-minimal-streams.md ├── 0061-minimal-streams │ └── subtyping.svg ├── 0062-memory-data.md ├── 0063-remove-lib-coding.md ├── 0065-format-struct-enum.md ├── 0066-simulation-time.md ├── 0069-simulation-port.md ├── 0070-soc-memory-map-names.md ├── 0071-enumview-matches.md ├── 0073-stricter-connections.md ├── 0074-structured-vcd.md ├── 0074-structured-vcd │ ├── gtkwave_post_rfc.png │ ├── gtkwave_pre_rfc.png │ ├── surfer_post_rfc.png │ └── surfer_pre_rfc.png └── 0076-amaranth-boards-versioning-policy.md └── theme └── img-color-scheme.css /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: Deploy 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out source code 11 | uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | - name: Install mdbook 15 | run: | 16 | mkdir mdbook 17 | curl -Lf https://github.com/rust-lang/mdBook/releases/download/v0.4.26/mdbook-v0.4.26-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook 18 | echo `pwd`/mdbook >> $GITHUB_PATH 19 | - name: Generate book 20 | run: | 21 | ./generate-book.py 22 | - name: Publish book 23 | uses: JamesIves/github-pages-deploy-action@v4 24 | with: 25 | repository-name: amaranth-lang/amaranth-lang.github.io 26 | ssh-key: ${{ secrets.PAGES_DEPLOY_KEY }} 27 | branch: main 28 | folder: book/ 29 | target-folder: rfcs/ 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | /book/ 3 | -------------------------------------------------------------------------------- /0000-template.md: -------------------------------------------------------------------------------- 1 | - Start Date: (fill in with date at which the RFC is merged, YYYY-MM-DD) 2 | - RFC PR: [amaranth-lang/rfcs#0000](https://github.com/amaranth-lang/rfcs/pull/0000) 3 | - Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000) 4 | 5 | # Name of the RFC 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | One paragraph explanation of the feature. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Why are we doing this? What use cases does it support? What is the expected outcome? 16 | 17 | ## Guide-level explanation 18 | [guide-level-explanation]: #guide-level-explanation 19 | 20 | Explain the proposal as if it was already included in the language and you were teaching it to another Amaranth programmer. That generally means: 21 | 22 | - Introducing new named concepts. 23 | - Explaining the feature largely in terms of examples. 24 | - Explaining how Amaranth programmers should *think* about the feature, and how it should impact the way they use Amaranth. It should explain the impact as concretely as possible. 25 | - If applicable, provide sample error messages, deprecation warnings, or migration guidance. 26 | - If applicable, describe the differences between teaching this to existing Amaranth programmers and new Amaranth programmers. 27 | - Discuss how this impacts the ability to read, understand, and maintain Amaranth code. Code is read and modified far more often than written; will the proposed feature make code easier to maintain? 28 | 29 | ## Reference-level explanation 30 | [reference-level-explanation]: #reference-level-explanation 31 | 32 | This is the technical portion of the RFC. Explain the design in sufficient detail that: 33 | 34 | - Its interaction with other features is clear. 35 | - It is reasonably clear how the feature would be implemented. 36 | - Corner cases are dissected by example. 37 | 38 | The section should return to the examples given in the previous section, and explain more fully how the detailed proposal makes those examples work. 39 | 40 | ## Drawbacks 41 | [drawbacks]: #drawbacks 42 | 43 | Why should we *not* do this? 44 | 45 | ## Rationale and alternatives 46 | [rationale-and-alternatives]: #rationale-and-alternatives 47 | 48 | - Why is this design the best in the space of possible designs? 49 | - What other designs have been considered and what is the rationale for not choosing them? 50 | - What is the impact of not doing this? 51 | - If this is a language proposal, could this be done in a library or macro instead? Does the proposed change make Amaranth code easier or harder to read, understand, and maintain? 52 | 53 | ## Prior art 54 | [prior-art]: #prior-art 55 | 56 | Discuss prior art, both the good and the bad, in relation to this proposal. Does this feature exist in other programming languages and what experience have their community had? 57 | 58 | This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. 59 | If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages. 60 | 61 | Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. 62 | Please also take into consideration that Amaranth sometimes intentionally diverges from common language features. 63 | 64 | ## Unresolved questions 65 | [unresolved-questions]: #unresolved-questions 66 | 67 | - What parts of the design do you expect to resolve through the RFC process before this gets merged? 68 | - What parts of the design do you expect to resolve through the implementation of this feature? 69 | - What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC? 70 | 71 | ## Future possibilities 72 | [future-possibilities]: #future-possibilities 73 | 74 | Think about what the natural extension and evolution of your proposal would be and how it would affect the language and project as a whole in a holistic way. 75 | Try to use this section as a tool to more fully consider all possible interactions with the project and language in your proposal. 76 | Also consider how this all fits into the roadmap for the project and of the relevant sub-team. 77 | 78 | This is also a good place to "dump ideas", if they are out of scope for the RFC you are writing but otherwise related. 79 | 80 | If you have tried and cannot think of any future possibilities, you may simply state that you cannot think of anything. 81 | 82 | Note that having something written down in the future-possibilities section is not a reason to accept the current or a future RFC; such notes should be in the section on motivation or rationale in this or subsequent RFCs. The section merely provides additional information. 83 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "The Amaranth RFC Book" 3 | 4 | [output.html] 5 | no-section-label = true 6 | git-repository-url = "https://github.com/amaranth-lang/rfcs" 7 | additional-css = ["theme/img-color-scheme.css"] 8 | 9 | [output.html.search] 10 | heading-split-level = 0 11 | 12 | [output.html.playground] 13 | runnable = false 14 | -------------------------------------------------------------------------------- /generate-book.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | This auto-generates the mdBook SUMMARY.md file based on the layout on the filesystem. 5 | This generates the `src` directory based on the contents of the `text` directory. 6 | Most RFCs should be kept to a single chapter. However, in some rare cases it 7 | may be necessary to spread across multiple pages. In that case, place them in 8 | a subdirectory with the same name as the RFC. For example: 9 | 0123-my-awesome-feature.md 10 | 0123-my-awesome-feature/extra-material.md 11 | It is recommended that if you have static content like images that you use a similar layout: 12 | 0123-my-awesome-feature.md 13 | 0123-my-awesome-feature/diagram.svg 14 | The chapters are presented in sorted-order. 15 | """ 16 | 17 | import os 18 | import shutil 19 | import subprocess 20 | 21 | def main(): 22 | if os.path.exists('src'): 23 | # Clear out src to remove stale links in case you switch branches. 24 | shutil.rmtree('src') 25 | os.mkdir('src') 26 | 27 | for path in os.listdir('text'): 28 | symlink(f'../text/{path}', f'src/{path}') 29 | symlink('../README.md', 'src/introduction.md') 30 | 31 | with open('src/SUMMARY.md', 'w') as summary: 32 | summary.write('[Introduction](introduction.md)\n\n') 33 | collect(summary, 'text', 0) 34 | 35 | subprocess.call(['mdbook', 'build']) 36 | 37 | def collect(summary, path, depth): 38 | entries = [e for e in os.scandir(path) if e.name.endswith('.md')] 39 | entries.sort(key=lambda e: e.name) 40 | for entry in entries: 41 | indent = ' '*depth 42 | name = entry.name[:-3] 43 | link_path = entry.path[5:] 44 | summary.write(f'{indent}- [{name}]({link_path})\n') 45 | maybe_subdir = os.path.join(path, name) 46 | if os.path.isdir(maybe_subdir): 47 | collect(summary, maybe_subdir, depth+1) 48 | 49 | def symlink(src, dst): 50 | if not os.path.exists(dst): 51 | os.symlink(src, dst) 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /text/0003-enumeration-shapes.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-02-27 2 | - RFC PR: [amaranth-lang/rfcs#3](https://github.com/amaranth-lang/rfcs/pull/3) 3 | - Amaranth Issue: [amaranth-lang/amaranth#756](https://github.com/amaranth-lang/amaranth/issues/756) 4 | 5 | # Enumeration shapes 6 | 7 | > **Amendments** 8 | > The behavior described in this RFC was updated by [RFC #4](0004-const-castable-exprs.md). 9 | 10 | ## Summary 11 | [summary]: #summary 12 | 13 | Allow explicitly specifying shape for enumerations as an alternative to implicitly inferring it. 14 | 15 | ## Motivation 16 | [motivation]: #motivation 17 | 18 | Hardware development includes a lot of enumerated values, so first class support for enumerations is important, and so is integration with the standard Python mechanisms for specifying enumerations. 19 | 20 | Amaranth accepts `enum.Enum` subclasses anywhere a shape is expected, and `enum.Enum` instances anywhere a value is expected: 21 | 22 | ```python 23 | >>> from amaranth import * 24 | >>> from enum import Enum 25 | >>> class Kind(Enum): 26 | ... MUL = 0 27 | ... ADD = 1 28 | ... SUB = 2 29 | ... 30 | >>> Shape.cast(Kind) 31 | unsigned(2) 32 | >>> Value.cast(Kind.SUB) 33 | (const 2'd2) 34 | ``` 35 | 36 | However, this does not cover an important use case: a enumeration where many values are reserved. For example, if the `Kind` enumeration above may need to be extended in the future, it would be necessary to reserve space for additional values, which may require additional storage bits. Right now there is no way to specify that `Kind` should be cast to e.g. `unsigned(4)`. 37 | 38 | ## Guide-level explanation 39 | [guide-level-explanation]: #guide-level-explanation 40 | 41 | The Amaranth standard library module, `amaranth.lib.enum` can be used as a drop-in replacement for the Python standard library `enum` module. It exports the same classes as the ones provided by Python's `enum` (namely `Enum`, `Flag`, `IntEnum`, and `IntFlag`) and provides the same functionality, adding the possibility of specifying a shape for the enumeration when it is defined: 42 | 43 | ```python 44 | >>> from amaranth.lib.enum import Enum 45 | >>> class Kind(Enum, shape=unsigned(4)): 46 | ... MUL = 0 47 | ... ADD = 1 48 | ... SUB = 2 49 | ... 50 | >>> Shape.cast(Kind) 51 | unsigned(4) 52 | >>> Value.cast(Kind.SUB) 53 | (const 4'd2) 54 | ``` 55 | 56 | If the `shape=` keyword argument is not specified, the enumeration is treated exactly the same as the corresponding standard Python class. 57 | 58 | If the values specified for the members are not representable with the explicitly provided shape, a warning is emitted: 59 | 60 | ```python 61 | >>> class Funct3(Enum, shape=unsigned(3)): 62 | ... SUB = 8 63 | ... 64 | :1: RuntimeWarning: Value of enumeration member will be truncated to enumeration shape unsigned(3) 65 | >>> class Funct3(Enum, shape=unsigned(3)): 66 | ... SUB = -1 67 | ... 68 | :1: RuntimeWarning: Value of enumeration member is signed, but enumeration shape is unsigned(3) 69 | ``` 70 | 71 | A shape that is specified for a base class will be inherited in subclasses: 72 | 73 | ```python 74 | >>> class Enum3(Enum, shape=unsigned(3)): pass 75 | ... 76 | >>> class Funct3(Enum3): 77 | ... SUB = 2 78 | ... 79 | >>> Shape.cast(Funct3) 80 | unsigned(3) 81 | ``` 82 | 83 | If a enumeration without an explicitly defined shape is used in a concatenation, a warning is emitted: 84 | 85 | ```python 86 | >>> class Kind(Enum): 87 | ... ADD = 1 88 | ... 89 | >>> Cat(Kind.ADD) 90 | :1: SyntaxWarning: Argument #1 of Cat() is an enumeration Kind.ADD without a defined shape used in bit vector context; define the enumeration by inheriting from the class in amaranth.lib.enum and specifying the 'shape=' keyword argument 91 | (cat (const 1'd1)) 92 | ``` 93 | 94 | ## Reference-level explanation 95 | [reference-level-explanation]: #reference-level-explanation 96 | 97 | The Amaranth standard library module, `amaranth.lib.enum`, exports all of the public names of the Python standard library `enum` module. The `EnumMeta` class adds the functionality for storing and casting to shapes, and inherits from `ShapeCastable`. The `Enum`, `Flag`, `IntEnum`, and `IntFlag` classes in this module derive from `enum.Enum`, `enum.Flag`, `enum.IntEnum`, and `enum.IntFlag` respectively, and use `amaranth.lib.enum.EnumMeta` as their metaclass, which makes subclasses of `amaranth.lib.enum.Enum`, etc be instances of `ShapeCastable`. 98 | 99 | When a new `amaranth.lib.enum.Enum` subclass is defined, `amaranth.lib.enum.EnumMeta.__new__` checks that the enumeration members are valid (currently, Amaranth requires these to be integers), and if the `shape=` argument is provided, stores it in an internal attribute. Importantly, the attribute is only set if the argument is provided, making it possible to distinguish these cases later. It also checks that all of the members can be represented by the given shape. 100 | 101 | When an `amaranth.lib.enum.Enum` subclass is cast to a shape, if the internal attribute is set, the shape in it is returned. Otherwise it is cast to a shape using exactly the same logic as what `Shape.cast` uses for `enum.Enum` subclasses. 102 | 103 | When an instance of a `enum.Enum` subclass is used in a concatenation, and it is not an instance of `ShapeCastable`, or if it lacks the `_amaranth_shape_` attribute, a warning is emitted. This approach avoids a circular dependency between `amaranth.hdl.ast` and `amaranth.lib.enum`. 104 | 105 | ## Drawbacks 106 | [drawbacks]: #drawbacks 107 | 108 | * Introducing a new standard library module increases the API surface. 109 | * The names of enumeration base classes are the same as the standard library enumeration base classes, which may be confusing. 110 | * Deriving from a different class requires changes to the enumeration at its point of definition, meaning that it is not possible to annotate a enum that comes from an external library with an Amaranth shape. 111 | 112 | ## Rationale and alternatives 113 | [rationale-and-alternatives]: #rationale-and-alternatives 114 | 115 | Ultimately, this feature boils down to defining an internal variable on an enum, which is then used by `Shape.cast` and other core Amaranth code. There are a few possible options for doing this. 116 | 117 | 1. Special class variable: 118 | 119 | ```python 120 | class SomeEnum(enum.Enum): 121 | _amaranth_shape_ = unsigned(4) 122 | ``` 123 | 124 | 2. Class decorator: 125 | 126 | ```python 127 | @amaranth.shape(unsigned(4)) 128 | class SomeEnum(enum.Enum): 129 | ``` 130 | 131 | 3. Class keyword argument (this proposal): 132 | 133 | ```python 134 | class SomeEnum(amaranth.lib.enum.Enum, shape=unsigned(4)): 135 | ``` 136 | 137 | Alternative (1) has the following drawbacks: 138 | * It is not possible to check that the enumeration members can be represented by the specified shape at the point of definition. 139 | * It exposes what should be an implementation detail to the user. 140 | * The documentation for the standard `enum` module does not specify whether it's OK to use `_sunder_` names for one's own purposes, but it would have to be a part of the stable API. 141 | 142 | Its advantages are: 143 | * No additional methods or classes in the API surface. 144 | * `_amaranth_shape_` makes it immediately clear what's going on. 145 | * The variable can be defined on any enum, even an external one. 146 | 147 | Alternative (2) has the following drawbacks: 148 | * It's not clear where the `shape` decorator should be. It can only be applied to enums, but there's no enum-specific namespace in Amaranth core to put it into. 149 | * `SomeEnum` would inherit from the standard `Enum` class and therefore `isinstance(SomeEnum, ShapeCastable)` would be `False` unless `ShapeCastable.__instancecheck__` is overridden to fix that. 150 | 151 | Its advantages are: 152 | * The decorator can be applied to an external enum. 153 | 154 | Alternative (3) has the drawbacks specified above, and the following advantages: 155 | * `isinstance(SomeEnum, ShapeCastable)` naturally works. 156 | * As a consequence, no additional code is required in the core language. All of the functionality necessary for the feature to work lives in `amaranth.lib.enum`. 157 | * The `shape` argument matches `Signal(shape=)` (even though no one uses the keyword form) and works the way one would naturally expect. 158 | * Uses of `import enum` can be transparently replaced with `from amaranth.lib import enum` without updating the call sites, making the migration as easy as the other alternatives. 159 | 160 | ## Prior art 161 | [prior-art]: #prior-art 162 | 163 | None. 164 | 165 | ## Unresolved questions 166 | [unresolved-questions]: #unresolved-questions 167 | 168 | None. 169 | 170 | ## Future possibilities 171 | [future-possibilities]: #future-possibilities 172 | 173 | None. 174 | -------------------------------------------------------------------------------- /text/0004-const-castable-exprs.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-02-07 2 | - RFC PR: [amaranth-lang/rfcs#4](https://github.com/amaranth-lang/rfcs/pull/4) 3 | - Amaranth Issue: [amaranth-lang/amaranth#755](https://github.com/amaranth-lang/amaranth/issues/755) 4 | 5 | # Const-castable expressions 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Define a subset of expressions that are "const-castable" and accept them in contexts where currently only integers and enumerations with integer values are accepted. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | In certain contexts, Amaranth requires a constant to be used. These contexts are: `with m.Case(...):`, `Value.matches(...)`, and the value of an enumeration member that is being cast to a value. 16 | 17 | Currently, only integers and enumeration members with integer values are considered constants. However, this is too limiting. For example, when developing a CPU, one might want to define control signals for several functional units and combine them into instructions, or conversely, match an instruction against a combination of control signals: 18 | 19 | ```python 20 | class Func(Enum): 21 | ADD = 0 22 | SUB = 1 23 | 24 | class Src(Enum): 25 | MEM = 0 26 | REG = 1 27 | 28 | class Instr(Enum): 29 | ADD = Cat(Func.ADD, Src.MEM) 30 | ADDI = Cat(Func.ADD, Src.REG) 31 | ... 32 | 33 | with m.Switch(instr): 34 | with m.Case(Cat(Func.ADD, Src.MEM)): 35 | ... 36 | ``` 37 | 38 | Currently, all of these cases would produce syntax errors. 39 | 40 | There is a private `Value._as_const` method. It is not used internally, however Amaranth developers have started using it due to unmet needs. Removing it without providing a replacement would be disruptive, and will result in downstream codebases defining their own equivalent. 41 | 42 | ## Guide-level explanation 43 | [guide-level-explanation]: #guide-level-explanation 44 | 45 | In any context where a constant is accepted (`with m.Case(...):`, `Value.matches(...)`, and the value of an enumeration member), a "const-castable" expression can be used. The following expressions are const-castable: 46 | * `int`; 47 | * `Const`; 48 | * `Cat` where all operands are const-castable; 49 | * instance of a `Enum` subclass where the value is const-castable. 50 | 51 | A const-castable expression can be converted to a `Const` using `Const.cast`: 52 | 53 | ```python 54 | >>> Const.cast(1) 55 | (const 1'd1) 56 | >>> Const.cast(Cat(1, 0, 1)) 57 | (const 3'd5) 58 | >>> Const.cast(Cat(Func.ADD, Src.REG)) 59 | (const 2'd2) 60 | ``` 61 | 62 | ## Reference-level explanation 63 | [reference-level-explanation]: #reference-level-explanation 64 | 65 | The `Const.cast` static method casts its argument to a value and examines it. If it is a `Const`, it is returned. If it is a const-castable expression, the operands are recursively cast to constants, and the expression is evaluated. 66 | 67 | The list of const-castable expressions is: 68 | * `Cat` 69 | 70 | The `m.Case(...)` (through the `Switch()` constructor) and `Value.matches(...)` methods accept two kinds of operands: const-castable expressions, or a string containing a bit pattern (a sequence of `0`, `1`, or `-` meaning a wildcard). 71 | 72 | The `Shape.cast` method accepts enumerations where all members have const-castable expressions as their values. The shape of an enumeration is a shape with the smallest width that can represent the value of any enumeration member. 73 | 74 | [RFC 3][]: The `EnumMeta.__new__` method accepts enumerations where all members have const-castable expressions as their values. The value of each member is the value of the constant resulting from casting the user-provided expression. 75 | 76 | [rfc 3]: 0003-enumeration-shapes.md 77 | 78 | ## Drawbacks 79 | [drawbacks]: #drawbacks 80 | 81 | - A new language-level concept makes it harder to learn the language. 82 | - Most developers already have an intuitive understanding of which expressions are const-castable. 83 | - `Const.cast` shadows an existing `Value.cast` method since `Const` inherits from `Value`. 84 | - No one is calling `Value.cast` through the `Const.cast` binding. 85 | - `Const.cast` has a compatible interface (it returns a `Value`) and performs a similar function (it calls `Value.cast` first). However, it's not Liskov-compatible. 86 | 87 | ## Rationale and alternatives 88 | [rationale-and-alternatives]: #rationale-and-alternatives 89 | 90 | Alternatives: 91 | 92 | 1. Do not add this functionality. Developers will define their own const-casting functions, continue to rely on the undocumented and private `._as_const()` method, or use other workarounds. 93 | 2. Make `._as_const()` public (i.e. rename it to `.as_const()`). 94 | 3. Add a new `Const.cast` method (this option). 95 | 96 | Alternatives (2) and (3) both introduce a new language-level concept, the only difference is in the interface that is used to access it. 97 | 98 | Alternative (3) fits the language better: `Value.cast` takes something value-castable and returns a `Value`, `Shape.cast` takes something shape-castable and returns a `Shape`, so `Const.cast` is a logical addition in that it takes something const-castable and returns a `Const`. 99 | 100 | ## Prior art 101 | [prior-art]: #prior-art 102 | 103 | Rust and C++ provide functionality (`const fn` and `constexpr` respectively) for performing computation at compile time, restricted to a strict subset of the full language. In particular, it can be used to initialize constants, which makes it similar to the functionality proposed here. 104 | 105 | One challenge these languages face is the question of how large the subset should be. Rust in particular started off heavily restricting `const fn`, where it did not have any control flow. The functionality was gradually introduced later as needed. 106 | 107 | ## Unresolved questions 108 | [unresolved-questions]: #unresolved-questions 109 | 110 | None. 111 | 112 | ## Future possibilities 113 | [future-possibilities]: #future-possibilities 114 | 115 | Expanding the set of const-castable expressions to include arbitrary arithmetic operations. This RFC limits it to the most requested expression, `Cat`. This simplifies implementation and reduces the likelihood of introducing bugs in the constant evaluation code, most of which would be almost never used. 116 | -------------------------------------------------------------------------------- /text/0005-remove-const-normalize.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-02-07 2 | - RFC PR: [amaranth-lang/rfcs#5](https://github.com/amaranth-lang/rfcs/pull/5) 3 | - Amaranth Issue: [amaranth-lang/amaranth#754](https://github.com/amaranth-lang/amaranth/issues/754) 4 | 5 | # Remove `Const.normalize` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Remove `Const.normalize(value, shape)`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | From the name it is not clear what it is supposed to achieve (it's truncation and inversion according to the shape) and it does not check types of arguments. 16 | 17 | We already have `Const(value, shape).value` and most developers should be aware of it. Having `Const.normalize(value, shape)` as well provides no benefit over the former. It's also longer. 18 | 19 | ## Explanation 20 | [explanation]: #explanation 21 | 22 | The `Const.normalize` method is deprecated (with the suggestion to use `Const().value`) and removed. 23 | 24 | ## Drawbacks 25 | [drawbacks]: #drawbacks 26 | 27 | - Churn. 28 | 29 | ## Rationale and alternatives 30 | [rationale-and-alternatives]: #rationale-and-alternatives 31 | 32 | We could keep it. Removing it reduces the API surface and makes the language a bit more elegant. 33 | 34 | ## Prior art 35 | [prior-art]: #prior-art 36 | 37 | None. 38 | 39 | ## Unresolved questions 40 | [unresolved-questions]: #unresolved-questions 41 | 42 | None. 43 | 44 | ## Future possibilities 45 | [future-possibilities]: #future-possibilities 46 | 47 | None. 48 | -------------------------------------------------------------------------------- /text/0008-aggregate-extensibility.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-05-11 2 | - RFC PR: [amaranth-lang/rfcs#8](https://github.com/amaranth-lang/rfcs/pull/8) 3 | - Amaranth Issue: [amaranth-lang/amaranth#772](https://github.com/amaranth-lang/amaranth/issues/772) 4 | 5 | # Aggregate data structure extensibility 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Provide well-defined extension points for the aggregate data structure library. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | [RFC 1]: 0001-aggregate-data-structures.md 16 | 17 | [RFC 1] introduces the aggregate data structure library, which allows using any shape-castable object as the shape of a field. Layouts do not consider the specific type of the shape-castable object. However, views do, and depending on whether it's a layout, a subclass of an aggregate class (`Struct` or `Union`), or any other shape-castable object, behavior differs: 18 | 19 | ```python 20 | >>> from amaranth import * 21 | >>> from amaranth.lib.data import * 22 | >>> class S(Struct): 23 | ... x: unsigned(1) 24 | ... 25 | >>> layout = StructLayout({ 26 | ... "a": unsigned(1), 27 | ... "b": ArrayLayout(unsigned(2), 4), 28 | ... "c": S 29 | ... }) 30 | ... 31 | >>> View(layout).a 32 | (slice (sig $signal) 0:1) 33 | >>> View(layout).b 34 | 35 | >>> View(layout).c 36 | <__main__.S object at 0x7f53e4693a30> 37 | ``` 38 | 39 | At the moment this behavior is not well-defined and it special-cases the aggregate classes defined in `amaranth.lib.data`. 40 | 41 | ## Guide-level explanation 42 | [guide-level-explanation]: #guide-level-explanation 43 | 44 | Any shape-castable object can be used as the shape of a field in a layout. This includes another layout. If the object is a callable (provides a `__call__` method), then when a view is accessed, the `__call__` method will be called with a single argument, the slice of the underlying value, which will be returned by the view. A `Layout` is a callable that constructs a `View` from itself and the value. 45 | 46 | ## Reference-level explanation 47 | [reference-level-explanation]: #reference-level-explanation 48 | 49 | The `Layout` class has a method `__call__`. `layout(value)` is equivalent to `View(layout, value)`. 50 | 51 | The `View.__getitem__` method (and by extension `View.__getattr__`), after extracting a `field` from the layout, attempts to call `field.shape.__call__(value_slice)`, where `value_slice` is the slice of the underlying value corresponding to the field. If there is no such method, it iteratively calls `as_shape()` on `field.shape` or the result of the previous call to `as_shape()` until an object is returned that has a `__call__` method, or until an instance of `Shape` is returned. 52 | 53 | ## Drawbacks 54 | [drawbacks]: #drawbacks 55 | 56 | * The syntax may be confusing. 57 | * Using `__call__` to implement construction is a widespread pattern in Python. Moreover, `Struct` and `Union` are classes, whose `__call__` method forwards to `__new__`, so implementing this behavior would remove the special case for aggregate base classes without additional code. 58 | 59 | ## Rationale and alternatives 60 | [rationale-and-alternatives]: #rationale-and-alternatives 61 | 62 | This design is, as far as the author knows, the smallest possible addition that provides the largest possible extensibility and removes all special casing of aggregate base classes. That it requires no additional functionality to be implemented on the aggregate base classes indicates that it fits well into the existing design. 63 | 64 | Alternatives: 65 | * Do not do this. 66 | 67 | ## Prior art 68 | [prior-art]: #prior-art 69 | 70 | None. 71 | 72 | ## Unresolved questions 73 | [unresolved-questions]: #unresolved-questions 74 | 75 | None. 76 | 77 | ## Future possibilities 78 | [future-possibilities]: #future-possibilities 79 | 80 | None. 81 | -------------------------------------------------------------------------------- /text/0009-const-init-shape-castable.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-05-11 2 | - RFC PR: [amaranth-lang/rfcs#9](https://github.com/amaranth-lang/rfcs/pull/9) 3 | - Amaranth Issue: [amaranth-lang/amaranth#771](https://github.com/amaranth-lang/amaranth/issues/771) 4 | 5 | # Constant initialization for shape-castable objects 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Add an extension point to shape-castable objects, for converting constant initializers (typically Python literals) to Amaranth constant expressions. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | [RFC 1]: 0001-aggregate-data-structures.md 16 | 17 | [RFC 1] specifies that the `reset=` argument of a `View` accepts structured data: 18 | 19 | ```python 20 | flt_neg_reset = data.View(float32_layout, reset={"sign": 1}) 21 | ``` 22 | 23 | This structured data is internally turned into an integer constant that is supplied to `Signal(reset=)`. This mechanism is not exposed to Amaranth developers. However, this creates a clear unmet need, since at the moment there is no way to turn a layout and a field-to-value mapping into a constant integer for use elsewhere. 24 | 25 | For example, if a signal is created manually and not through the `View`, this will not work despite looking reasonable (the layout is shape-castable and can be supplied to `Signal`): 26 | 27 | ```python 28 | flt_neg_reset_signal = Signal(float32_layout, reset={"sign": 1}) 29 | ``` 30 | 31 | ## Guide-level explanation 32 | [guide-level-explanation]: #guide-level-explanation 33 | 34 | Shape-castable objects must, in addition to the mandatory `.as_shape()` method, implement a mandatory `.const(value)` method to define how a constant initializer (the reset value of a `Signal` or `View`) is converted to an Amaranth constant. 35 | 36 | This method is defined by shape-castable objects to convert arbitrary Python objects into Amaranth constants. For example, if a shape-castable object has complex internal structure, it can accept a dictionary with the values to be filled into various bits of this structure. If `Shape` implemented `ShapeCastable`, the method would be defined as `def const(self, value): return Const(value, self)`. 37 | 38 | The value returned by this method can be a `Const` or a value-castable object whose `.as_value()` will return a `Const`. 39 | 40 | This method can also be directly called by the developer to construct a constant using a given shape-castable object. 41 | 42 | ## Reference-level explanation 43 | [reference-level-explanation]: #reference-level-explanation 44 | 45 | A method `def const(self, obj):` is added on `ShapeCastable`. 46 | 47 | The `Signal(shape, reset=)` constructor is changed so that if `isinstance(shape, ShapeCastable)`, then `shape.const(reset)` is used instead of `reset`. 48 | 49 | The `.const()` instance method is implemented on `Layout` to accept a `Sequence` or `Mapping` instance and returns a `View` over a `Const` with a bit pattern that has the fields set to the given values. Overlapping fields are written in the order of iteration of the input. If the field shape is a shape-castable object, then the value for that field is computed using `Const.cast(value, field.shape)`. 50 | 51 | The `.const()` method is implemented on the metaclass of `Struct` and `Union` as `return View(self, self.as_shape().const(obj))`. 52 | 53 | The `View(..., reset=)` constructor is changed to pass `reset` through to the `Signal()` constructor. 54 | 55 | ## Drawbacks 56 | [drawbacks]: #drawbacks 57 | 58 | * Additional method on `ShapeCastable`. 59 | * It was clear from the beginning that this functionality will likely be necessary, and we are unlikely to ever add more. 60 | * The `reset=` argument becomes dependently typed. 61 | 62 | ## Rationale and alternatives 63 | [rationale-and-alternatives]: #rationale-and-alternatives 64 | 65 | Given: 66 | 67 | ```python 68 | class Point(Struct): 69 | x: 16 70 | y: 16 71 | ``` 72 | 73 | it is clear that there needs to be some way to go from `{"x": 123, "y": 456}` to `Cat(C(123, 16), C(456, 16))` without manually writing out the concatenation. 74 | 75 | There are two main options for this: 76 | 1. Implement a new method, such that `Point.const({"x": 123, "y": 456})` returns a constant of some kind (either an `int` or a `Const` or a constant-castable expression). 77 | 2. Implement an additional `.__init__()` override, such that `Point({"x": 123, "y": 456})` returns a view that has a constant-castable expression as its target. 78 | 79 | Option (1) has the benefit of making it clear when downstream code is creating a constant (and expects an argument where the nested data must all be constant), and of minimizing useless wrapping/unwrapping of views that would otherwise happen. It is an explicit type conversion (from a literal to `Const`). 80 | 81 | Option (2) avoids introducing new names. It is an implicit type conversion (from a literal to a view, which in this case is `Point`). 82 | 83 | In the end, option (1) seems preferable here since implicit type conversions are easy to unintentionally misuse. It also avoids any clashes with proposed RFC 8. 84 | 85 | ## Prior art 86 | [prior-art]: #prior-art 87 | 88 | None. 89 | 90 | ## Unresolved questions 91 | [unresolved-questions]: #unresolved-questions 92 | 93 | None. 94 | 95 | ## Future possibilities 96 | [future-possibilities]: #future-possibilities 97 | 98 | None. 99 | -------------------------------------------------------------------------------- /text/0010-move-repl-to-value.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-05-11 2 | - RFC PR: [amaranth-lang/rfcs#10](https://github.com/amaranth-lang/rfcs/pull/10) 3 | - Amaranth Issue: [amaranth-lang/amaranth#770](https://github.com/amaranth-lang/amaranth/issues/770) 4 | 5 | # Move `Repl` to `Value.replicate` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Replace the standalone `Repl(value, count)` node with `value.replicate(count)`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | `Repl` is a [rarely used](https://github.com/search?q=%2F%5CbRepl%5Cb%2F+amaranth+language%3Apython&type=code) construct (it's mostly useful for manual sign extension). 16 | 17 | It is currently a first-class entity that has its own AST node and a name in the prelude, mostly for historical reasons (`Repl(v, n)` is analogous to Verilog's `{x{n}}`). 18 | 19 | `Repl` does not need to be a first-class entity; `Repl(x, n)` is almost exactly equivalent to `Cat(x for x in range(n))`. It especially does not need a name in the prelude. 20 | 21 | ## Guide-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | Use of `Repl` is deprecated. To replicate a value multiple times, use `value.replicate()`. 25 | 26 | ## Reference-level explanation 27 | [reference-level-explanation]: #reference-level-explanation 28 | 29 | Direct use of `Repl` is deprecated. Its implementation is replaced with `def Repl(value, count): return Value.cast(value).replicate(count)`. 30 | 31 | A function `Value.replicate(count)` is added. It is implemented as `Cat(value for _ in range(count))`. The `Repl` AST node is removed. 32 | 33 | ## Drawbacks 34 | [drawbacks]: #drawbacks 35 | 36 | * Churn. 37 | * The proposed implementation makes `Value.replicate` valid on left-hand side of assignment, with potentially surprising behavior. However, this can be handled by prohibiting multiple assignment to the same bit of a signal in general. 38 | 39 | ## Rationale and alternatives 40 | [rationale-and-alternatives]: #rationale-and-alternatives 41 | 42 | Rationale: 43 | 44 | * Fewer names in the prelude is always good. 45 | * Unlike with `Cat` (where `Cat()` makes sense), `Repl` does not make sense as a standalone node any more than `Part` does (and we do not currently export `Part`). 46 | * Despite existing by analogy with `{x{n}}`, it is currently turned into a concatenation before it reaches the Verilog backend *anyway*, and any future work will have to reconstruct replication from concatenation in any case. 47 | * `Repl` being a dedicated node complicates AST processing for no reason. 48 | 49 | Alternatives: 50 | 51 | * Do not do this. 52 | 53 | ## Prior art 54 | [prior-art]: #prior-art 55 | 56 | None. 57 | 58 | ## Unresolved questions 59 | [unresolved-questions]: #unresolved-questions 60 | 61 | None. 62 | 63 | ## Future possibilities 64 | [future-possibilities]: #future-possibilities 65 | 66 | * The Verilog backend currently bitblasts what could be a replication. We could detect these and convert them to replications proper. 67 | * We could detect code like `Cat(x, x).eq(0b11)` and warn or reject it. 68 | -------------------------------------------------------------------------------- /text/0015-lifting-shape-castables.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-05-15 2 | - RFC PR: [amaranth-lang/rfcs#15](https://github.com/amaranth-lang/rfcs/pull/15) 3 | - Amaranth Issue: [amaranth-lang/amaranth#784](https://github.com/amaranth-lang/amaranth/issues/784) 4 | 5 | # Lifting shape-castable objects 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Make `Signal(shape_castable, ...)` return `shape_castable(Signal(shape_castable.as_shape(), ...))`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | When Amaranth was a very new language, it did not have any facilities for ascribing type information to data. It had shapes (width and signedness), and it had special handling for `range()` in the shape position, as well as enumerations. Back then it made sense to have `Signal`, the single way to define new storage of any kind, to only operate on values (numbers / bit containers). 16 | 17 | Today the situation is completely different. Amaranth has first-class support for enumerations in the standard library as well as the standard range of data structures (structs, unions, arrays) via [RFC 1] and [RFC 3]. It provides extensibility through [RFC 8] and [RFC 9]. Using the existing hooks alone it is possible to extend Amaranth with rich numeric types (fixed-point, complex, potentially even floating-point), and some of these are very likely to end up in the standard library. 18 | 19 | All of this new functionality internally wraps a `Value`. It is so common and useful to initialize e.g. a struct view with a fresh `Signal` that `data.View` reexports all of the arguments of the `Signal` constructors and automatically constructs a `Signal` if no view target is provided. This works, but ties the two together more than would be ideal, and requires every similar facility to reimplement the functionality itself. What is worse is that it seems to be quite confusing to programmers, since it's not apparent that calling `data.View(foo_layout)` internally creates a `Signal`. Furthermore, people want to call `Signal(foo_layout)` to construct some storage for `foo_layout`, and that works (`foo_layout` is shape-castable), but does the wrong thing: the returned object is a `Signal`, not a `data.View`. 20 | 21 | It would make teaching a lot easier if we could draw an equivalence between a `Signal` and a variable in a general purpose programming language, and between its shape and a type in a general purpose programming language. Then, no matter what shape-castable object it is, the way to make some storage is `Signal(x)`. It will also simplify the internals a fair bit. 22 | 23 | This change wasn't practical before [RFC 8] and [RFC 9] laid the groundwork for it, but now it is an obvious extension. 24 | 25 | [RFC 1]: 0001-aggregate-data-structures.md 26 | [RFC 3]: 0003-enumeration-shapes.md 27 | [RFC 8]: 0008-aggregate-extensibility.md 28 | [RFC 9]: 0009-const-init-shape-castable.md 29 | 30 | ## Guide-level explanation 31 | [guide-level-explanation]: #guide-level-explanation 32 | 33 | To include state in a design, use the `Signal(shape)` constructor, where `shape` describes the bit layout and possible operations on that state. The `reset=` argument and the returned value depend on the `shape` that is provided. If it is `signed(N)` or `unsigned(N)` or a built-in enumeration or a `range`, then a plain `Value` is returned, and the `reset=` argument accepts a number, an enumeration member, or a constant. If it is a `data.Layout`, then a `data.View` is returned, and the `reset=` argument accepts a sequence or a mapping, potentially nested for nested layouts. Other shape-castable classes will have their own behavior. 34 | 35 | > **Warning** 36 | > The existing syntax for creating a `View` with a new `Signal` underlying it will be removed immediately (it has never been in a release) to resolve an ambiguity over the semantics of `__call__`. 37 | 38 | ## Reference-level explanation 39 | [reference-level-explanation]: #reference-level-explanation 40 | 41 | A method `def __call__(self, value):` is added on `ShapeCastable`. It must return `Value` or a `ValueCastable` instance with the right shape. (Such a method is opportunistically used by `data.View` for nested views since [RFC 8], however this RFC makes it mandatory for all shape-castable objects.) 42 | 43 | The `Signal.__call__(shape, ...)` method is overridden (on the metaclass) to consider `shape`. First, a `signal` is constructed normally with all of the arguments. Afterwards, if `shape` is a `ShapeCastable` instance, then `shape(signal)` is returned. Otherwise `signal` is returned. 44 | 45 | ## Drawbacks 46 | [drawbacks]: #drawbacks 47 | 48 | * Increase in language complexity. 49 | * More metaclasses. 50 | * `Signal` is a final class so this is unlikely to go wrong. 51 | * A `Signal()` constructor sometimes returning non-`Signal` objects can be confusing. 52 | 53 | ## Rationale and alternatives 54 | [rationale-and-alternatives]: #rationale-and-alternatives 55 | 56 | There are several arguments in favor of the design: 57 | * It does not de facto introduce any new methods on protocols, since `ShapeCastable.__call__` is expected to be implemented by essentially everyone after [RFC 8]. 58 | * It does not introduce new complexity to `Signal.__init__`; the logic for handling non-integer reset exists since [RFC 9]. 59 | * It eliminates unnecessary coupling between `data.View` (and other similar facilities) and `Signal()`. 60 | * It is a natural extension of the language and has clear parallels to the notion of variables in other languages. 61 | * It has been repeatedly requested by users, almost every time someone became familiar with the aggregate data structure design. 62 | 63 | All of these points are compelling but the last one perhaps the most. The author did not find it a stark enough necessity to introduce themselves but it does seem to be one. 64 | 65 | Alternatives: 66 | * Do not do this. The status quo is acceptable. 67 | 68 | ## Prior art 69 | [prior-art]: #prior-art 70 | 71 | This RFC brings the semantics of `Signal` to be very close to semantics of typed variables in other languages. 72 | 73 | "Lifting" in the title of this RFC refers to a [concept in functional programming](https://wiki.haskell.org/Lifting) of the same name where a higher order function (`Signal`, here) is used to generalize an operation over a set of other functions (`data.View` and other shape-castable objects that implement the `__call__` protocol, here). 74 | 75 | ## Unresolved questions 76 | [unresolved-questions]: #unresolved-questions 77 | 78 | * How does this interact with typechecking? 79 | * This is a straightforward higher order function so it's probably fine. 80 | 81 | ## Future possibilities 82 | [future-possibilities]: #future-possibilities 83 | 84 | This RFC is the final one in a chain that started with [RFC 1]. 85 | 86 | Enumerations and ranges could be adjusted such that something other than `Value` is returned. This creates backwards compatibility concerns though. 87 | -------------------------------------------------------------------------------- /text/0017-remove-log2-int.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-01-08 2 | - RFC PR: [amaranth-lang/rfcs#17](https://github.com/amaranth-lang/rfcs/pull/17) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1025](https://github.com/amaranth-lang/amaranth/issues/1025) 4 | 5 | # Remove `log2_int` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Replace `log2_int` with two functions: `ceil_log2` and `exact_log2`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | `log2_int` is a helper function that was copied from Migen in the early days of Amaranth. 16 | 17 | It behaves like so: 18 | 19 | * `n` must be an integer, and a power-of-2 unless `need_pow2` is `False`; 20 | * if `n == 0`, it returns `0`; 21 | * if `n != 0`, it returns `(n - 1).bit_length()`. 22 | 23 | #### Differences with `math.log2` 24 | 25 | In practice, `log2_int` differs from `math.log2` in the following ways: 26 | 27 | 1. its implementation is restricted to integers only; 28 | 2. if `need_pow2` is false, the result is rounded up to the nearest integer; 29 | 3. it doesn't raise an exception for `n == 0`; 30 | 4. if `need_pow2` is false, it doesn't raise an exception for `n < 0`. 31 | 32 | #### Observations 33 | 34 | * *1)* is a desirable property; coercing integers into floating-point numbers is fraught with peril, as the latter have limited precision. 35 | * *2)* has common use-cases in digital design, such as address decoders. 36 | * *3)* and *4)* are misleading at best. Despite being advertised as a logarithm, `log2_int` doesn't exclude 0 or negative integers from its domain. 37 | 38 | ## Guide-level explanation 39 | [guide-level-explanation]: #guide-level-explanation 40 | 41 | Amaranth provides two log2 functions for integer arithmetic: 42 | 43 | * `ceil_log2(n)`, where `n` is assumed to be any non-negative integer 44 | * `exact_log2(n)`, where `n` is assumed to be an integer power-of-2 45 | 46 | For example: 47 | 48 | ```python3 49 | ceil_log2(8) # 3 50 | ceil_log2(5) # 3 51 | ceil_log2(4) # 2 52 | 53 | exact_log2(8) # 3 54 | exact_log2(5) # raises a ValueError 55 | exact_log2(4) # 2 56 | ``` 57 | 58 | ## Reference-level explanation 59 | [reference-level-explanation]: #reference-level-explanation 60 | 61 | Use of the `log2_int` function is deprecated. 62 | 63 | A `ceil_log2(n)` function is added, that: 64 | 65 | * returns the integer log2 of the smallest power-of-2 greater than or equal to `n`; 66 | * raises a `TypeError` if `n` is not an integer; 67 | * raises a `ValueError` if `n` is lesser than 0. 68 | 69 | An `exact_log2(n)` function is added, that: 70 | 71 | * returns the integer log2 of `n`; 72 | * raises a `TypeError` if `n` is not an integer; 73 | * raises a `ValueError` if `n` is not a power-of-two. 74 | 75 | ## Drawbacks 76 | [drawbacks]: #drawbacks 77 | 78 | This is a breaking change. 79 | 80 | ## Rationale and alternatives 81 | [rationale-and-alternatives]: #rationale-and-alternatives 82 | 83 | The following alternatives have been considered: 84 | 85 | 1. Do nothing. Callers of `log2_int` may still need to restrict its domain to positive integers. 86 | 2. Restrict `log2_int` to positive integers. Downstream code relying on the previous behavior may silently break. 87 | 3. Remove `log2_int`, and use `math.log2` as replacement: 88 | * `log2_int(n)` would be replaced with `math.log2(n)` 89 | * `log2_int(n, need_pow2=False)` would be replaced with `math.ceil(math.log2(n))` 90 | 91 | Option *3)* will give incorrect results, as `n` is coerced from `int` to `float`: 92 | 93 | ``` 94 | >>> log2_int((1 << 64) + 1, need_pow2=False) 95 | 65 96 | >>> math.ceil(math.log2((1 << 64) + 1)) 97 | 64 98 | ``` 99 | 100 | ## Prior art 101 | [prior-art]: #prior-art 102 | 103 | None. 104 | 105 | ## Unresolved questions 106 | [unresolved-questions]: #unresolved-questions 107 | 108 | None. 109 | 110 | ## Future possibilities 111 | [future-possibilities]: #future-possibilities 112 | 113 | None. 114 | 115 | ## Acknowledgements 116 | 117 | [@wanda-phi] provided valuable feedback while this RFC was being drafted. 118 | 119 | [@wanda-phi]: https://github.com/wanda-phi 120 | -------------------------------------------------------------------------------- /text/0018-reorganize-vendor-platforms.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-08-22 2 | - RFC PR: [amaranth-lang/rfcs#18](https://github.com/amaranth-lang/rfcs/pull/18) 3 | - Amaranth Issue: [amaranth-lang/amaranth#873](https://github.com/amaranth-lang/amaranth/issues/873) 4 | 5 | # Reorganize vendor platforms 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Update `amaranth.vendor` namespace so that instead of: 11 | 12 | ```python 13 | from amaranth.vendor.lattice_ecp5 import LatticeECP5Platform 14 | ``` 15 | 16 | you would write: 17 | 18 | ```python 19 | from amaranth.vendor import LatticeECP5Platform 20 | ``` 21 | 22 | 23 | ## Motivation 24 | [motivation]: #motivation 25 | 26 | Vendor names are ever-changing. Xilinx was bought by AMD and the brand has been phased out. Altera was bought by Intel and the brand has been phased out. SiliconBlue has been bought by Lattice (a long time ago) and the brand has *long* been phased out but still remains as "SB" in `SB_LUT` primitive name. 27 | 28 | In addition, we attempt to group FPGA families into a single file, like `vendor.lattice_machxo2_3l` that has been renamed from `vendor.lattice_machxo2`. This will likely include another FPGA family as soon as it becomes available. 29 | 30 | By tying module (and file) names to brand names we create churn. Every Amaranth release so far has included renaming of both platform class names and module names. This causes additional downstream breakage and annoys designers using Amaranth. 31 | 32 | ## Guide-level explanation 33 | [guide-level-explanation]: #guide-level-explanation 34 | 35 | To target your FPGA-based project for a particular FPGA family, import the platform class corresponding to the FPGA family from `amaranth.vendor`, e.g.: 36 | 37 | ```python 38 | from amaranth.vendor import LatticeECP5Platform 39 | ``` 40 | 41 | ## Reference-level explanation 42 | [reference-level-explanation]: #reference-level-explanation 43 | 44 | All of the `amaranth.vendor.name` modules are renamed to `amaranth.vendor._internal_name`. 45 | 46 | Python allows `__getattr__` to be present in modules: 47 | 48 | $ cat >x.py 49 | def __getattr__(self, name): 50 | return f"__getattr__({name!r})" 51 | $ python 52 | >>> from x import abc 53 | >>> abc 54 | "__getattr__('abc')" 55 | 56 | This allows us to make all the platform classes be present as-if they were defined in the `amaranth.vendor` modules, while retaining all of the benefits of having them in their own `amaranth.vendor._internal_name` module, such as lazy loading. 57 | 58 | ## Drawbacks 59 | [drawbacks]: #drawbacks 60 | 61 | - Churn. 62 | - A somewhat unusual loading mechanism could cause confusion. 63 | 64 | ## Rationale and alternatives 65 | [rationale-and-alternatives]: #rationale-and-alternatives 66 | 67 | Decoupling marketing/brand names from technical names is increasingly important as Amaranth evolves and supports more FPGA families. It allows us to maintain any internal hierarchy we want without it having any impact on downstream code, which solely operates on names imported from `amaranth.vendor`. 68 | 69 | ## Prior art 70 | [prior-art]: #prior-art 71 | 72 | None. 73 | 74 | ## Unresolved questions 75 | [unresolved-questions]: #unresolved-questions 76 | 77 | None. 78 | 79 | ## Future possibilities 80 | [future-possibilities]: #future-possibilities 81 | 82 | None. 83 | -------------------------------------------------------------------------------- /text/0019-remove-scheduler.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-08-22 2 | - RFC PR: [amaranth-lang/rfcs#19](https://github.com/amaranth-lang/rfcs/pull/19) 3 | - Amaranth Issue: [amaranth-lang/amaranth#874](https://github.com/amaranth-lang/amaranth/issues/874) 4 | 5 | # Remove `amaranth.lib.scheduler` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Remove `amaranth.lib.scheduler` and the only class `RoundRobin` in it. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | This module is not used in the sole place for which it was added (Amaranth SoC), it is not especially useful, and it has not undergone proper community review when it was added. 16 | 17 | ## Guide-level explanation 18 | [guide-level-explanation]: #guide-level-explanation 19 | 20 | The module `amaranth.lib.scheduler` and the sole class `RoundRobin` in it is removed. To continue using it, copy the contents of the module into your own project. 21 | 22 | ## Reference-level explanation 23 | [reference-level-explanation]: #reference-level-explanation 24 | 25 | The class `amaranth.lib.scheduler.RoundRobin` is deprecated in Amaranth 0.4 and removed in Amaranth 0.5. 26 | 27 | ## Drawbacks 28 | [drawbacks]: #drawbacks 29 | 30 | - Churn. 31 | 32 | ## Rationale and alternatives 33 | [rationale-and-alternatives]: #rationale-and-alternatives 34 | 35 | - This module is out of place in the standard library. 36 | - It has not seen much use and is trivially implemented outside of it. 37 | - Downstream consumers tend to inline the logic anyway. 38 | - It does not seem like there would be any other uses for the `amaranth.lib.scheduler` module since any other scheduling algorithm would be more closely tied to the consumer. 39 | 40 | ## Unresolved questions 41 | [unresolved-questions]: #unresolved-questions 42 | 43 | None. 44 | 45 | ## Future possibilities 46 | [future-possibilities]: #future-possibilities 47 | 48 | None. 49 | -------------------------------------------------------------------------------- /text/0020-deprecate-non-fwft-fifos.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-08-22 2 | - RFC PR: [amaranth-lang/rfcs#20](https://github.com/amaranth-lang/rfcs/pull/20) 3 | - Amaranth Issue: [amaranth-lang/amaranth#875](https://github.com/amaranth-lang/amaranth/issues/875) 4 | 5 | # Deprecate non-FWFT FIFOs 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Deprecate non-first-word-fall-through FIFOs. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Currently, FIFOs in `amaranth.lib.fifo` have two incompatible interfaces: FWFT (first word fallthrough) and non-FWFT. The incompatibility concerns only the read half. FWFT FIFOs have `r_data` valid when `r_rdy` is asserted. Non-FWFT FIFOs have `r_data` valid only after strobing `r_en`, if the FIFO was empty previously. 16 | 17 | Non-FWFT interface is awkward and is essentially never used. It is a holdover from Migen and its implementation details that was included for compatibility. There are three downsides to having it: 18 | 1. Having non-FWFT FIFOs requires every consumer of the FIFO interface to check for `fwft` when interacting with the FIFO and either asserting that it is `True`, or adding a code path to handle it. No one does this. 19 | 2. The FWFT interface is directly compatible with streams and allows us to add e.g. `r_stream` and `w_stream` to existing FIFOs without adding a wrapper such as `stream.FIFO`. It also makes any custom FIFOs defined downstream of Amaranth stream-enabled. 20 | 3. The notion of FWFT vs non-FWFT FIFOs is confusing and difficult to understand. E.g. the author of this RFC wrote both `lib.fifo` and the Glasgow FIFO code, and she misused the `fwft` argument in the latter. 21 | 22 | ## Guide-level and reference-level explanation 23 | [guide-level-explanation]: #guide-level-explanation 24 | 25 | In the next version, instantiating `SyncFIFO(fwft=False)` emits a deprecation warning. In addition, `FIFOInterface`'s `fwft` parameter now defaults to `True`. Other FIFOs have no non-FWFT variant in the first place. 26 | 27 | In the version after that, there is no way to instantiate `SyncFIFO(fwft=False)`. The feature and all references to it are removed in their entirety. 28 | 29 | ## Implementation considerations 30 | [reference-level-explanation]: #reference-level-explanation 31 | 32 | At the moment, `SyncFIFOBuffered` is implemented as a register in the output of `SyncFIFO(fwft=False)`. The implementation will need to be rewritten. 33 | 34 | ## Drawbacks 35 | [drawbacks]: #drawbacks 36 | 37 | - Churn. 38 | - There will be no alternative to `SyncFIFO(fwft=False)`. 39 | 40 | ## Rationale and alternatives 41 | [rationale-and-alternatives]: #rationale-and-alternatives 42 | 43 | - It is feasible to extract `SyncFIFO(fwft=False)` into its own module that may be used by downstream code that needs non-FWFT FIFOs. It would not implement `FIFOInterface`. 44 | - There is no reason the `SyncFIFO` class could not be copied into downstream code as it is. 45 | - It is possible to wrap FIFOs in the stream library in a way that ensures only FWFT FIFOs are used. 46 | - Let's not create pointless wrappers. 47 | 48 | ## Prior art 49 | [prior-art]: #prior-art 50 | 51 | Not relevant. 52 | 53 | ## Unresolved questions 54 | [unresolved-questions]: #unresolved-questions 55 | 56 | None. 57 | 58 | ## Future possibilities 59 | [future-possibilities]: #future-possibilities 60 | 61 | This RFC primarily exists to enable better stream interface integration. 62 | -------------------------------------------------------------------------------- /text/0021-patch-releases.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-08-22 2 | - RFC PR: [amaranth-lang/rfcs#21](https://github.com/amaranth-lang/rfcs/pull/21) 3 | 4 | # Patch releases 5 | 6 | ## Summary 7 | [summary]: #summary 8 | 9 | Change Amaranth versioning from `major.minor` to `major.minor.patch` after version 0.4, and define the backport policy for patch releases. 10 | 11 | ## Motivation 12 | [motivation]: #motivation 13 | 14 | Amaranth 0.3 was released on 2021-12-16; almost two years ago. Several important bugs have been fixed in `main` since, most notably depending on a version of `Jinja2` that is no longer installable. At the moment the policy is to issue only `major.minor` releases, which was OK in the early days but no longer fits the project. 15 | 16 | We should change the policy that is used for the next Amaranth release and later ones. 17 | 18 | ## Explanation 19 | [guide-level-explanation]: #guide-level-explanation 20 | 21 | Amaranth feature releases all have the version of `major.minor.0`. The policy for these releases is unchanged and is tied to our two-step process for making breaking changes. 22 | 23 | In addition to these releases, Amaranth now has bug-fix releases with the `major.minor.patch` versions. These are intended to address the need of the community to have bugs fixed before a next feature release can be made, and the policy is designed to minimize developer time spent on them. 24 | 25 | Bug-fix releases are made when all of the following conditions are satisfied: 26 | - There is an issue that is fixed in the `main` branch. 27 | - A member of the community requests this issue to be fixed in a point release. 28 | - It is possible to fix the issue such that there is a high degree of confidence that the change will not break existing code using Amaranth with a `~=major.minor` version constraint. 29 | - A community member steps up to backport the fix to the release branch. 30 | - This could be one of the Amaranth maintainers, or anyone else. Amaranth maintainers have no obligation to back-port any fix. 31 | 32 | ## Drawbacks 33 | [drawbacks]: #drawbacks 34 | 35 | This creates additional work for maintainers. 36 | 37 | ## Rationale and alternatives 38 | [rationale-and-alternatives]: #rationale-and-alternatives 39 | 40 | - It would be possible to backport all feasible fixes as a policy. 41 | - This would significantly increase maintainer workload. 42 | - It is possible to keep the current policy. 43 | - Because we do not control all of the upstream dependencies (including Python), this seems untenable. 44 | 45 | ## Prior art 46 | [prior-art]: #prior-art 47 | 48 | Rust has an even stricter bug-fix release policy: the Rust project only issues patch releases in cases of security issues, widespread miscompilations, or unintentional breaking changes. 49 | 50 | ## Unresolved questions 51 | [unresolved-questions]: #unresolved-questions 52 | 53 | Should Amaranth SoC adopt the same policy? 54 | 55 | ## Future possibilities 56 | [future-possibilities]: #future-possibilities 57 | 58 | Eventually, Amaranth may gain release engineers who will maintain long-living release branches. 59 | -------------------------------------------------------------------------------- /text/0022-valuecastable-shape.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-08-22 2 | - RFC PR: [amaranth-lang/rfcs#22](https://github.com/amaranth-lang/rfcs/pull/22) 3 | - Amaranth Issue: [amaranth-lang/amaranth#876](https://github.com/amaranth-lang/amaranth/issues/876) 4 | 5 | # Define `ValueCastable.shape()` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Require value-castable objects to have a method that returns their high-level shape. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | [RFC #15][] advanced the extensibility of the language significantly, but broke constructs like `Signal.like(Signal(data.StructLayout(...)))`. In addition, not having a well-defined point for returning the high-level shape (i.e. a shape-castable object from which this value-castable object was creaed rather than a `Shape` instance) causes workarounds such as `data.Layout.of` to be added to the language and standard library. 16 | 17 | [RFC #15]: 0015-lifting-shape-castables.md 18 | 19 | ## Explanation 20 | [guide-level-explanation]: #guide-level-explanation 21 | 22 | The `ValueCastable` interface has a method `.shape()`. This method returns a shape-castable object. Where possibe, this object should, when passed to `Signal`, create a value-castable object of the same type. 23 | 24 | `amaranth.lib.data.Layout.of` is removed immediately. 25 | 26 | ## Drawbacks 27 | [drawbacks]: #drawbacks 28 | 29 | - Increased API surface area 30 | - At one point a commitment was made that the only method `ValueCastable` will ever define will be `as_value`. However, radical changes to the language such as [RFC #15][] make it reasonable to revisit this. 31 | 32 | ## Rationale and alternatives 33 | [rationale-and-alternatives]: #rationale-and-alternatives 34 | 35 | This change is the minimal possible one that fixes the problem systemically. Some minor variations in the design are possible: 36 | - Instead of requiring `shape()` to be defined (which is a breaking change), this method can be added optionally in the next release and be required in the release after that. 37 | - This is difficult to do with `ValueCastable` and will require workarounds both in the core language implementation and in downstream code that operates on `ValueCastable` objects. 38 | - `ValueCastable` is not very widely used yet and the breakage will likely be minimal. 39 | 40 | ## Prior art 41 | [prior-art]: #prior-art 42 | 43 | It is typical for a programming language to have a way of retrieving the type of a value. The mechanism being added here is equivalent. 44 | 45 | ## Unresolved questions 46 | [unresolved-questions]: #unresolved-questions 47 | 48 | None. 49 | 50 | ## Future possibilities 51 | [future-possibilities]: #future-possibilities 52 | 53 | None. 54 | -------------------------------------------------------------------------------- /text/0027-simulator-testbenches.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-02-05 2 | - RFC PR: [amaranth-lang/rfcs#27](https://github.com/amaranth-lang/rfcs/pull/27) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1082](https://github.com/amaranth-lang/amaranth/issues/1082) 4 | 5 | # Testbench processes for the simulator 6 | 7 | > **Amendments** 8 | > This RFC was amended on 2024-02-12 to deprecate `add_sync_process` rather than `add_process`, for two reasons: 9 | > 1. `add_process` encompasses anything `add_sync_process` can do, but there is functionality that is quite difficult to do with `add_sync_process`, such as behavioral implementation of a DDR flop. 10 | > 2. `add_sync_process` relies on argument-less `yield`, which has no equivalent with `await ...` syntax that is desired in the future. 11 | 12 | ## Summary 13 | [summary]: #summary 14 | 15 | The existing `Simulator.add_sync_process` method causes the process function to observe the design in a state before combinational settling, something that is actively unhelpful in testbenches. A new `Simulator.add_testbench` method will only return control to the process function after combinational settling. 16 | 17 | ## Motivation 18 | [motivation]: #motivation 19 | 20 | Consider the following code: 21 | 22 | ```python 23 | from amaranth import * 24 | from amaranth.sim import Simulator 25 | 26 | 27 | class DUT(Elaboratable): 28 | def __init__(self): 29 | self.out = Signal() 30 | self.outn = Signal() 31 | 32 | def elaborate(self, platform): 33 | m = Module() 34 | m.d.sync += self.outn.eq(~self.out) 35 | return m 36 | 37 | 38 | dut = DUT() 39 | def testbench(): 40 | yield dut.out.eq(1) 41 | yield 42 | print((yield dut.out)) 43 | print((yield dut.outn)) 44 | 45 | sim = Simulator(dut) 46 | sim.add_clock(1e-6) 47 | sim.add_sync_process(testbench) 48 | sim.run() 49 | ``` 50 | 51 | This code prints: 52 | 53 | ``` 54 | 1 55 | 1 56 | ``` 57 | 58 | While this result is sensible in a behavioral implementation of an elaboratable (where observing the state of the outputs of combinational cells before they transition to the new state is required for such an implementation to function as a drop-in replacement for a register transfer level one), it is not something a testbench should ever print; it clearly contradicts the netlist. Because there are no alternatives to using `add_sync_process`, testbenches (where such result is completely inappropriate) keep using it, and Amaranth designers are left to sprinkle `yield` over the testbenches until the result works. 59 | 60 | In addition to the direct impact of this issue, it also prevents building reusable abstractions, including something as simple as `yield from fifo.read()`, since in order to work for back-to-back reads that would first have to `yield Settle()` to observe the updated value of `fifo.r_rdy`, which isn't appropriate for a function in the standard library as it changes the observable behavior (and thus breaks the abstraction). 61 | 62 | ## Guide-level explanation 63 | [guide-level-explanation]: #guide-level-explanation 64 | 65 | The code example above is rewritten as: 66 | 67 | ```python 68 | dut = DUT() 69 | def testbench(): 70 | yield dut.out.eq(1) 71 | yield Tick() 72 | print((yield dut.out)) 73 | print((yield dut.outn)) 74 | 75 | sim = Simulator(dut) 76 | sim.add_clock(1e-6) 77 | sim.add_testbench(testbench) 78 | sim.run() 79 | ``` 80 | 81 | When run, it prints: 82 | 83 | ``` 84 | 1 85 | 0 86 | ``` 87 | 88 | Existing testbenches can be ported to use `Simulator.add_testbench` by removing extraneous `yield` or `yield Settle()` calls (and, in some cases, shifting other `yield` calls around). 89 | 90 | Reusable abstractions can be built by defining generator functions on interfaces or components. 91 | 92 | ### Guidance on simulator modalities 93 | 94 | There are two main simulator modalities: `add_testbench` and `add_process`. They have completely disjoint purposes: 95 | 96 | - `add_testbench` is used for testing logic (asynchronous or synchronous). It is not used for behavioral replacement of synchronous logic. 97 | - `add_process` is used for behavioral replacement of synchronous logic. It is not for testing logic (except for legacy code), and a deprecation warning is shown when `yield Settle()` is executed in such a process. 98 | 99 | Example of using `add_testbench` to test combinatorial logic: 100 | 101 | ```python 102 | m = Module() 103 | m.d.comb += a.eq(~b) 104 | 105 | def testbench(): 106 | yield b.eq(1) 107 | print((yield a)) # => 0 108 | 109 | sim = Simulator(m) 110 | # No clock is required 111 | sim.add_testbench(testbench) 112 | sim.run() 113 | ``` 114 | 115 | Example of using `add_testbench` to test synchronous logic: 116 | 117 | ```python 118 | m = Module() 119 | m.d.sync += a.eq(~b) 120 | 121 | def testbench(): 122 | yield b.eq(1) 123 | yield Tick() # same as Tick("sync") 124 | print((yield a)) # => 0 125 | 126 | sim = Simulator(m) 127 | sim.add_clock(1e-6) 128 | sim.add_testbench(testbench) 129 | sim.run() 130 | ``` 131 | 132 | Example of using `add_process` to replace the flop above, and `add_testbench` to test the flop: 133 | 134 | ```python 135 | m = Module() 136 | 137 | def flop(): 138 | while True: 139 | yield b.eq(~(yield a)) 140 | yield Tick() 141 | 142 | def testbench(): 143 | yield b.eq(1) 144 | yield Tick() # same as Tick("sync") 145 | print((yield a)) # => 0 146 | 147 | sim = Simulator(m) 148 | sim.add_clock(1e-6) 149 | sim.add_process(flop) 150 | sim.add_testbench(testbench) 151 | sim.run() 152 | ``` 153 | 154 | ### Why not replace `add_process` with `add_testbench` entirely? 155 | 156 | It is not possible to use `add_testbench` processes that drive signals in a race-free way. Consider this (behaviorally defined) circuit: 157 | 158 | ```python 159 | x = Signal(reset=1) 160 | y = Signal() 161 | 162 | def proc_flop(): 163 | yield Tick() 164 | yield y.eq(x) 165 | 166 | def proc2(): 167 | yield Tick() 168 | xv = yield x 169 | yv = yield y 170 | print(f"proc2 x={xv} y={yv}") 171 | 172 | def proc3(): 173 | yield Tick() 174 | yv = yield y 175 | xv = yield x 176 | print(f"proc3 x={xv} y={yv}") 177 | ``` 178 | 179 | If these processes are added using `add_testbench`, the output is: 180 | 181 | ``` 182 | proc3 x=1 y=0 183 | proc2 x=1 y=1 184 | ``` 185 | 186 | If they are added using `add_process`, the output is: 187 | 188 | ``` 189 | proc2 x=1 y=0 190 | proc3 x=1 y=0 191 | ``` 192 | 193 | Clearly, if `proc2` and `proc3` are other flops in the circuit, perhaps performing a computation on `x` and `y`, they must be simulated using `add_process`. 194 | 195 | ## Reference-level explanation 196 | [reference-level-explanation]: #reference-level-explanation 197 | 198 | A new `Simulator.add_testbench(process)` is added. This function schedules `process` similarly to `add_process`, except that before returning control to the coroutine `process` it performs the equivalent of `yield Settle()`. 199 | 200 | `add_sync_process` and `Settle` are deprecated and removed in a future version. 201 | 202 | ## Drawbacks 203 | [drawbacks]: #drawbacks 204 | 205 | - Churn. 206 | - Testbench processes can race with each other, and it is not trivial to use multiple testbench processes in a design in a race-free way. 207 | - Processes using `Settle` can race as well. 208 | 209 | ## Rationale and alternatives 210 | [rationale-and-alternatives]: #rationale-and-alternatives 211 | 212 | The motivating issue has no known alternative resolution besides introducing this (or a very similar) API. The status quo has proved deeply unsatisfactory over many years, and the `add_testbench` process has been trialed in 2020 and found usable. 213 | 214 | ## Prior art 215 | [prior-art]: #prior-art 216 | 217 | Other simulators experience similar challenges with event scheduling. In Verilog, this is one of the reasons for the use of blocking assignment `=`. Where the decision of the scheduling primitive is left to the point of use (rather than the point of declaration, as proposed in this RFC) it leads to complexity in teaching the concept. 218 | 219 | ## Unresolved questions 220 | [unresolved-questions]: #unresolved-questions 221 | 222 | None. 223 | 224 | ## Future possibilities 225 | [future-possibilities]: #future-possibilities 226 | 227 | In the standard library, `fifo.read()` and `fifo.write()` functions could be defined that aid in testing designs with FIFOs. Such functions will only work correctly within testbench processes. 228 | 229 | As it is, every such helper function would have to take a `domain` argument, which can quickly get out of hand. We have `DomainRenamer` in the RTL sub-language and we may want to have something like that in the simulation sub-language. (@zyp) 230 | 231 | A new `add_comb_process` function could be added, to replace combinatorial logic. This function would have to accept a list of all signals driven by the process, so that combinatorial loops could be detected. (The demand for this has not been high; as of right now, this is not possible anyway.) 232 | 233 | The existing `add_process` function could accept a list of all signals driven by the process. This could aid in error detection, especially as CXXRTL is integrated into the design, because if a simulator process is driving a signal at the same time as an RTL process, a silent race condition occurs. 234 | -------------------------------------------------------------------------------- /text/0028-override-value-operators.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-10-30 2 | - RFC PR: [amaranth-lang/rfcs#0028](https://github.com/amaranth-lang/rfcs/pull/0028) 3 | - Amaranth Issue: [amaranth-lang/amaranth#0929](https://github.com/amaranth-lang/amaranth/issues/0929) 4 | 5 | # Allow overriding Value operators 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Allow overriding binary `Value` operators with reflected operators in a value-castable type. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | A value-castable type can define operators that return another value-castable. 16 | However, if the left side operand is a `Value`, its operator will be called first, casting the right side operand to a plain `Value`. 17 | This creates a mismatch in behavior depending on the type and order of operands. 18 | 19 | As an example, consider the multiplication of a fixed point value-castable with an integral type: 20 | ``` 21 | >>> Q(7).const(0.5) * 255 22 | (fixedpoint Q8.7 (* (const 8'sd64) (const 8'd255))) 23 | >>> 255 * Q(7).const(0.5) 24 | (fixedpoint Q8.7 (* (const 8'sd64) (const 8'd255))) 25 | >>> Q(7).const(0.5) * C(255) 26 | (fixedpoint Q8.7 (* (const 8'sd64) (const 8'd255))) 27 | >>> C(255) * Q(7).const(0.5) 28 | (* (const 8'd255) (const 8'sd64)) 29 | ``` 30 | 31 | ## Explanation 32 | [guide-level-explanation]: #guide-level-explanation 33 | 34 | When a binary `Value` operator is called with a value-castable `other`, check whether the value-castable implements the reflected variant of the operator first and defer to it when present. 35 | 36 | ## Drawbacks 37 | [drawbacks]: #drawbacks 38 | 39 | Extra logic required around every `Value` operator. 40 | 41 | ## Prior art 42 | [prior-art]: #prior-art 43 | 44 | This is standard behavior for inheritance in [Python](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types): 45 | 46 | > Note: If the right operand’s type is a subclass of the left operand’s type and that subclass provides a different implementation of the reflected method for the operation, this method will be called before the left operand’s non-reflected method. This behavior allows subclasses to override their ancestors’ operations. 47 | 48 | We don't get this behavior automatically because `Value` is not an ancestor of `ValueCastable`, but it would make sense for it to behave as it were. 49 | 50 | ## Rationale and alternatives 51 | [rationale-and-alternatives]: #rationale-and-alternatives 52 | 53 | As an alternative, `Value` and `ValueCastable` could be rearchitected so that `ValueCastable` inherits from either `Value` or a common base that implements the `Value` operators. 54 | This would make Python do the right thing w.r.t. operator overriding, but is a larger change with more potential for undesirable consequences. 55 | 56 | 57 | ## Unresolved questions 58 | [unresolved-questions]: #unresolved-questions 59 | 60 | None. 61 | 62 | ## Future possibilities 63 | [future-possibilities]: #future-possibilities 64 | 65 | `Value.eq()` could in the same manner check for and defer to a `.req()` method, i.e. reflected `.eq()`, to allow a value-castable to override how assignment from it to a `Value` is handled. 66 | -------------------------------------------------------------------------------- /text/0031-enumeration-type-safety.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-11-27 2 | - RFC PR: [amaranth-lang/rfcs#31](https://github.com/amaranth-lang/rfcs/pull/31) 3 | - Amaranth Issue: [amaranth-lang/amaranth#972](https://github.com/amaranth-lang/amaranth/issues/972) 4 | 5 | # Enumeration type safety 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Make Amaranth `Enum` and `Flag` use a custom `ValueCastable` view class, enforcing type safety. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Python `Enum` provides an opaque wrapper over the underlying enum values, 16 | providing type safety and guarding against improper usage in arithmetic 17 | operations: 18 | 19 | ```pycon 20 | >>> from enum import Enum 21 | >>> class EnumA(Enum): 22 | ... A = 0 23 | ... B = 1 24 | ... 25 | >>> EnumA.A + 1 26 | Traceback (most recent call last): 27 | File "", line 1, in 28 | TypeError: unsupported operand type(s) for +: 'EnumA' and 'int' 29 | ``` 30 | 31 | Likewise, `Flag` values can be used in bitwise operations, but only within 32 | their own type: 33 | 34 | ```pycon 35 | >>> from enum import Flag 36 | >>> class FlagA(Flag): 37 | ... A = 1 38 | ... B = 2 39 | ... 40 | >>> class FlagB(Flag): 41 | ... C = 1 42 | ... D = 2 43 | ... 44 | >>> FlagA.A | FlagA.B 45 | 46 | >>> FlagA.A | FlagB.C 47 | Traceback (most recent call last): 48 | File "", line 1, in 49 | TypeError: unsupported operand type(s) for |: 'FlagA' and 'FlagB' 50 | ``` 51 | 52 | However, these safety properties are not currently enforced by Amaranth 53 | on enum-typed signals: 54 | 55 | ```pycon 56 | >>> from amaranth import * 57 | >>> from amaranth.lib.enum import * 58 | >>> class FlagA(Flag): 59 | ... A = 1 60 | ... B = 2 61 | ... 62 | >>> class FlagB(Flag): 63 | ... C = 1 64 | ... D = 2 65 | ... 66 | >>> a = Signal(FlagA) 67 | >>> b = Signal(FlagB) 68 | >>> a | b 69 | (| (sig a) (sig b)) 70 | ``` 71 | 72 | 73 | ## Guide-level explanation 74 | [guide-level-explanation]: #guide-level-explanation 75 | 76 | Like in Python, `Enum` and `Flag` subclasses are considered strongly-typed, 77 | while `IntEnum` and `IntFlag` are weakly-typed. Enum-typed Amaranth values 78 | with strong typing are manipulated through `amaranth.lib.enum.EnumView` 79 | and `amaranth.lib.enum.FlagView` classes, which wrap an underlying `Value` 80 | in a type-safe container that only allows a small subset of operations. 81 | For weakly-typed enums, `Value` is used directly, providing full 82 | interchangeability with other values. 83 | 84 | An `EnumView` or a `FlagView` can be obtained by: 85 | 86 | - Creating an enum-typed signal (`a = Signal(MyEnum)`) 87 | - Explicitly casting a value to the enum type (`MyEnum(value)`) 88 | 89 | The operations available on `EnumView` and `FlagView` include: 90 | 91 | - Comparing for equality to another view of the same enum type (`a == b` and `a != b`) 92 | - Assigning to or from a value 93 | - Converting to a plain value via `Value.cast` 94 | 95 | The operations additionally available on `FlagView` include: 96 | 97 | - Binary bitwise operations with another `FlagView` of the same type 98 | (`a | b`, `a & b`, `a ^ b`) 99 | - Bitwise inversion (`~a`) 100 | 101 | A custom subclass of `EnumView` or `FlagView` can be used for a given enum 102 | type if so desired, by using the `view_class` keyword parameter on enum 103 | creation. 104 | 105 | ## Reference-level explanation 106 | [reference-level-explanation]: #reference-level-explanation 107 | 108 | `amaranth.lib.enum.EnumView` is a `ValueCastable` subclass. The following 109 | operations are defined on it: 110 | 111 | - `EnumView(enum, value_castable)`: creates the view 112 | - `shape()`: returns the underlying enum 113 | - `as_value()`: returns the underlying value 114 | - `eq(value_castable)`: delegates to `eq` on the underlying value 115 | - `__eq__` and `__ne__`: if the other argument is an `EnumView` of the same 116 | enum type or a value of the enum type, delegates to the corresponding 117 | `Value` operator; otherwise, raises a `TypeError` 118 | - All binary arithmetic, bitwise, and remaining comparison operators: raise 119 | a `TypeError` (to override the implementation provided by `Value` in case 120 | of an operation between `EnumView` and `Value`) 121 | 122 | `amaranth.lib.enum.FlagView` is a subclass of `EnumView`. The following 123 | additional operations are defined on it: 124 | 125 | - `__and__`, `__or__`, `__xor__`: if the other argument is a `FlagView` 126 | of the same enum type or a value of the enum type, delegates to the 127 | corresponding `Value` operator and wraps the result in `FlagView`; 128 | otherwise, raises a `TypeError` 129 | - `__invert__`: inverts all bits in this value corresponding to actually 130 | defined flags in the underlying enum type, then wraps the result in 131 | `FlagView` 132 | 133 | The behavior of `EnumMeta.__call__` when called on a value-castable 134 | is changed as follows: 135 | 136 | - If the enum has been created with a `view_class`, the value-castable 137 | is wrapped in the given class 138 | - Otherwise, if the enum type is a subclass of `IntEnum` or `IntFlag`, the 139 | value-castable is returned as a plain `Value` 140 | - Otherwise, if the enum type is a subclass of `Flag`, the value-castable 141 | is wrapped in `FlagView` 142 | - Otherwise, the value-castable is wrapped in `EnumView` 143 | 144 | The behavior of `EnumMeta.const` is modified to go through the same logic. 145 | 146 | ## Drawbacks 147 | [drawbacks]: #drawbacks 148 | 149 | This proposal increases language complexity, and is not consistent with 150 | eg. how `amaranth.lib.data.View` operates (which has much more lax type 151 | checking). 152 | 153 | ## Rationale and alternatives 154 | [rationale-and-alternatives]: #rationale-and-alternatives 155 | 156 | Do nothing. Operations on mismatched types will continue to be silently 157 | allowed. 158 | 159 | Equality could work more like Python equality (always returning false 160 | for mismatched types). 161 | 162 | Assignment could be made strongly-typed as well (with corresponding hook 163 | added to `Value`). 164 | 165 | ## Prior art 166 | [prior-art]: #prior-art 167 | 168 | This feature directly parallels the differences between Python's 169 | `Enum`/`Flag` and `IntEnum`/`IntFlag`. 170 | 171 | ## Unresolved questions 172 | [unresolved-questions]: #unresolved-questions 173 | 174 | Instead of having an extension point via `view_class`, we could instead 175 | automatically forward all otherwise unknown methods to the underlying enum 176 | class, providing it the `EnumView` as `self`. 177 | 178 | ## Future possibilities 179 | [future-possibilities]: #future-possibilities 180 | 181 | None. -------------------------------------------------------------------------------- /text/0034-interface-rename.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-12-04 2 | - RFC PR: [amaranth-lang/rfcs#34](https://github.com/amaranth-lang/rfcs/pull/34) 3 | - Amaranth Issue: [amaranth-lang/amaranth#985](https://github.com/amaranth-lang/amaranth/issues/985) 4 | 5 | # Rename `amaranth.lib.wiring.Interface` to `PureInterface` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | The `Interface` class in `amaranth.lib.wiring` is renamed to `PureInterface`, to avoid the impression that it is used for *all* interfaces. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | The current naming of the `Interface` class wrongly suggests that it is the base class to be used for all interfaces, and that `isinstance(foo, Interface)` is a valid check for an interface. However, this is in stark contrast to how `lib.wiring` works: any object can be an interface, as long as it has a `signature` property and compliant members. This misleads users (and, on at least two occasions, amaranth developers), making them write buggy code. 16 | 17 | Additionally, the naming makes spoken language ambiguous in a bad way, as it is impossible to tell apart "an interface" and "an Interface". 18 | 19 | Therefore, this RFC proposes to rename `Interface` to something more specific and reflecting its function. 20 | 21 | ## Guide-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | The `Interface` class in `amaranth.lib.wiring` is renamed to `PureInterface`. 25 | 26 | ## Reference-level explanation 27 | [reference-level-explanation]: #reference-level-explanation 28 | 29 | The `Interface` class in `amaranth.lib.wiring` is renamed to `PureInterface`. 30 | 31 | ## Drawbacks 32 | [drawbacks]: #drawbacks 33 | 34 | Minor churn. 35 | 36 | ## Rationale and alternatives 37 | [rationale-and-alternatives]: #rationale-and-alternatives 38 | 39 | The new name is, of course, subject to bikeshedding. The names that have been proposed are: 40 | 41 | - `PureInterface` 42 | - `BareInterface` 43 | 44 | ## Prior art 45 | [prior-art]: #prior-art 46 | 47 | None. 48 | 49 | ## Unresolved questions 50 | [unresolved-questions]: #unresolved-questions 51 | 52 | None. 53 | 54 | ## Future possibilities 55 | [future-possibilities]: #future-possibilities 56 | 57 | The name `Interface` that has just been freed up can be reused for an ABC-like class representing all valid interfaces. 58 | -------------------------------------------------------------------------------- /text/0035-shapelike-valuelike.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-12-04 2 | - RFC PR: [amaranth-lang/rfcs#35](https://github.com/amaranth-lang/rfcs/pull/35) 3 | - Amaranth Issue: [amaranth-lang/amaranth#986](https://github.com/amaranth-lang/amaranth/issues/986) 4 | 5 | # Add `ShapeLike`, `ValueLike` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Two special classes are added to the language: `ShapeLike` and `ValueLike`. They cannot be constructed, but can be used to determine with `isinstance` and `issubclass` to determine whether something can be cast to `Shape` or a `Value`, respectively. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | As it stands, we have multiple types of objects that can be used as shapes (`Shape`, `ShapeCastable`, `int`, `range`, `EnumMeta`) and values (`Value`, `ValueCastable`, `int`, `Enum`). These types have no common superclass, so there's no easy way to check if an object can be used as a shape or a value, save for actually calling `Shape.cast` or `Value.cast`. Introducing `ShapeLike` and `ValueLike` provides an idiomatic way to perform such a check. 16 | 17 | Additionally, when type annotations are in use, there is currently no simple type that can be used for an argument that takes an arbitrary shape- or value-castable object. These new classes provide such a simple type. 18 | 19 | ## Guide-level explanation 20 | [guide-level-explanation]: #guide-level-explanation 21 | 22 | In Amaranth, multiple types of objects can be cast to shapes: 23 | 24 | - actual `Shape` objects 25 | - `ShapeCastable` objects 26 | - non-negative integers 27 | - `range` objects 28 | - `Enum` subclasses with const-castable values 29 | 30 | To check whether an object is of a type that can be cast to a shape, `isinstance(obj, ShapeLike)` can be used. To check whether a type can be, in general, cast to a shape, `issubclass(cls, ShapeLike)` can be used. 31 | 32 | Likewise, multiple types of objects can be cast to values: 33 | 34 | - actual `Value` objects 35 | - `ValueCastable` objects 36 | - integers 37 | - values of `Enum` subclasses with const-castable values 38 | 39 | To check whether an object is of a type that can be cast to a value, `isinstance(obj, ValueLike)` can be used. To check whether a type can be, in general, cast to a value, `issubclass(cls, ValueLike)` can be used. 40 | 41 | ## Reference-level explanation 42 | [reference-level-explanation]: #reference-level-explanation 43 | 44 | A `ShapeLike` class is provided. It cannot be constructed, and can only be used with `isinstance` and `issubclass`, which are overriden by a custom metaclass. 45 | 46 | `issubclass(cls, ShapeLike)` returns `True` for: 47 | 48 | - `Shape` 49 | - `ShapeCastable` and its subclasses 50 | - `int` and its subclasses 51 | - `range` and its subclasses 52 | - `enum.EnumMeta` and its subclasses 53 | 54 | `isinstance(obj, ShapeLike)` returns `True` for: 55 | 56 | - instances of `Shape` 57 | - instances of `ShapeCastable` and its subclasses 58 | - non-negative `int` values (and `int` subclasses) 59 | - `enum.Enum` subclasses where every value is a `ValueLike` 60 | 61 | Similarly, a `ValueLike` class is provided. 62 | 63 | `issubclass(cls, ValueLike)` returns `True` for: 64 | 65 | - `Value` and its subclasses 66 | - `ValueCastable` and its subclasses 67 | - `int` and its subclasses 68 | - `enum.Enum` subclasses where every value is a `ValueLike` 69 | 70 | `isinstance(obj, ValueLike)` returns `True` iff `issubclass(type(obj), ValueLike)` returns `True`. 71 | 72 | ## Drawbacks 73 | [drawbacks]: #drawbacks 74 | 75 | More moving parts in the language. 76 | 77 | `isinstance(obj, ShapeLike)` does not actually guarantee that `Shape.cast(obj)` will succeed — the instance check looks only at surface-level information, and an exception can still be thrown. `issubclass(cls, ShapeLike)` is, by necessity, even more inaccurate. 78 | 79 | ## Rationale and alternatives 80 | [rationale-and-alternatives]: #rationale-and-alternatives 81 | 82 | There are many ways to implement the instance and subclass checks, some more precise (and complex) than others. The semantics described above are a compromise. 83 | 84 | For `isinstance`, a simple variant would be to just try `Shape.cast` or `Value.cast` and see if it raises an exception. However, this will sometimes result in `isinstance(MyShapeCastable(), ShapeLike)` returning `False`, which may be very unintuitive and hide bugs. 85 | 86 | The check for a valid shape-castable enum described above is an approximation — the actual logic used requires all values of an enum to be *const*-castable, not just value-castable. However, there is no way to check this without actually invoking `Value.cast` on the enum members. 87 | 88 | ## Prior art 89 | [prior-art]: #prior-art 90 | 91 | Python has the concept of abstract base classes, such as `collections.abc.Sequence`, which can be used for subclass checking even if they are not actual superclasses of the types involved. `ShapeLike` and `ValueLike` are effectively ABCs, though they do not use the actual ABC machinery (due to having custom logic in instance checking). 92 | 93 | ## Unresolved questions 94 | [unresolved-questions]: #unresolved-questions 95 | 96 | - Should the exact details of the instance and subclass checks be changed? 97 | 98 | ## Future possibilities 99 | [future-possibilities]: #future-possibilities 100 | 101 | A similar ABC-like class has been proposed for `lib.wiring` interfaces. 102 | -------------------------------------------------------------------------------- /text/0037-make-signature-immutable.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-12-11 2 | - RFC PR: [amaranth-lang/rfcs#37](https://github.com/amaranth-lang/rfcs/pull/37) 3 | - Amaranth Issue: [amaranth-lang/amaranth#995](https://github.com/amaranth-lang/amaranth/issues/995) 4 | 5 | # Make `Signature` immutable 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Remove mutability from `amaranth.lib.wiring.Signature`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | At the time of writing, `Signature` allows limited mutability: members can be added, but not removed or changed; and a signature may be frozen, preventing further mutation. 16 | 17 | The intent behind this feature was unfortunately never explicitly described. I (Catherine) came up with it, to cover the following case: suppose an interface without certain features needs to be connected to an interface with such features. For example, a Wishbone initiator that does not handle errors needs to be connected to a Wishbone decoder that does. In this case, a method on the Wishbone initiator's interface could be used to add a dummy `err` output, which will always remain at its reset value, zero. 18 | 19 | This intent was never realized as the feature was never actually used by Amaranth SoC. In addition, it turned out to be problematic when combined with variable annotations. Consider this class definition: 20 | 21 | ```py 22 | class SomeInitiator(wiring.Component): 23 | bus: Out(wishbone.Interface(...)) 24 | ``` 25 | 26 | In this case, only one instance of `wishbone.Interface` is created. Mutating this instance would be unsound, since it would affect every instance of the component and not just the one for that particular one, and when this causes issues this would be very difficult to debug. 27 | 28 | The presence of this feature encouraged adding other mutable objects to `Signature` subclasses, such as memory maps. That is also unsound, for similar reasons. Because memory maps in Amaranth SoC can also be frozen, considerable additional complexity was introduced since piecewise freezing was now possible. 29 | 30 | Because freezing was defined as a property of the signature members dictionary, overloading `Signature.freeze` was meaningless: it would be possible, in rare but legal cases, to have a signature frozen without `freeze` being called for that signature, leaving any additional objects that should have been frozen mutable. 31 | 32 | The `wiring.connect` function also supports constants as both inputs and outputs, where a constant input can be connected to a constant output provided their values match. This behavior is also suited for implementing optional features (where the absence of an optional feature means the corresponding ports are fixed at a constant value), and does not pose any hazards. 33 | 34 | ## Guide-level and reference-level explanation 35 | [guide-level-explanation]: #guide-level-explanation 36 | 37 | The `SignatureMembers.freeze`, `SignatureMembers.frozen`, `Signature.freeze`, `Signature.frozen`, `FlippedSignatureMembers.freeze`, `FlippedSignatureMembers.frozen`, `FlippedSignature.freeze`, `FlippedSignature.frozen`, `SignatureMembers.__iadd__`, `SignatureMembers.__setitem__`, `FlippedSignatureMembers.__iadd__`, `FlippedSignatureMembers.__setitem__` methods and properties are removed. 38 | 39 | `SignatureMembers` becomes an immutable mapping. 40 | 41 | `Signature` becomes immutable. Subclasses of `Signature` are required to ensure any additional methods or properties do not allow mutation. 42 | 43 | ## Drawbacks 44 | [drawbacks]: #drawbacks 45 | 46 | The author is not aware of anyone actually using this feature. 47 | 48 | ## Rationale and alternatives 49 | [rationale-and-alternatives]: #rationale-and-alternatives 50 | 51 | An alternative to this proposal would be to automatically freeze any signature that is used in `wiring.Component` variable annotations. This does not address similar hazards, such as the case of a user-defined constant `SomeSignature = Signature({...})`, which is also a legitimate way to define a signature that does not require parameterization. It also would not address hazards associated with interior mutability. 52 | 53 | ## Prior art 54 | [prior-art]: #prior-art 55 | 56 | None seems applicable. Mutation of interface definitions is uncommon in first place (excluding languages where everything is mutable, like Python). 57 | 58 | ## Unresolved questions 59 | [unresolved-questions]: #unresolved-questions 60 | 61 | - Should *all* interior mutability be prohibited? At the moment it is not completely clear where memory maps should be attached, and requiring no interior mutability would mean it cannot be `Signature` no matter what. Interior mutability could be left to specific `Signature` subclasses instead on an experimental basis, and prohibited later if it turns out to be a bad idea. 62 | 63 | ## Future possibilities 64 | [future-possibilities]: #future-possibilities 65 | 66 | Reintroducing mutability of `Signature` after it has been removed will be unfeasible due to expectation of immutability being baked in widely in downstream code. Once we commit to this RFC we will have to commit to it effectively forever. -------------------------------------------------------------------------------- /text/0038-component-signature-immutability.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2023-12-11 2 | - RFC PR: [amaranth-lang/rfcs#38](https://github.com/amaranth-lang/rfcs/pull/38) 3 | - Amaranth Issue: [amaranth-lang/amaranth#996](https://github.com/amaranth-lang/amaranth/issues/996) 4 | 5 | # `Component.signature` immutability 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Clearly define the contract for `amaranth.lib.wiring.Component.signature`: an `amaranth.lib.wiring.Signature` object is assigned in the constructor to the read-only property `.signature`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | It is important that the signature of an interface object never change. `connect` relies on the signature to check that the connection being made is valid; if the signature changes after the connection is made (intentionally or accidentally), the design would silently break in a difficult to debug ways. For an ASIC this may mean a respin. 16 | 17 | Right now, the guidance for the case where the default behavior of `Component.signature` is insufficient (i.e. for parametric components) and the generation of the signature must be customized, is to override the `signature` property, or to assign `self.signature =` in the constructor. This is clearly wrong: both of these approaches can easily result in the signature changing after construction of the component. 18 | 19 | Moreover, at the moment, the implementation of the `Component.signature` property creates a new `Signature` object every time it is called. If the identity of the `Signature` object is important (i.e. if it has interior mutability, which is currently the case in Amaranth SoC), this is unsound. (It is unlikely, though not impossible, that this implementation would return an object with different members.) 20 | 21 | ## Guide-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | To define a simple component whose signature does not change, use variable annotations: 25 | 26 | ```python 27 | class SimpleComponent(wiring.Component): 28 | en: In(1) 29 | data: Out(8) 30 | ``` 31 | 32 | To define a component whose signature is parameterized by constructor arguments, call the superclass constructor with the signature that should be applied to the component: 33 | 34 | ```python 35 | class ParametricComponent(wiring.Component): 36 | def __init__(self, data_width): 37 | super().__init__({ 38 | "en": In(1), 39 | "data": Out(data_width) 40 | }) 41 | ``` 42 | 43 | Do not override the `signature` property, as both Amaranth and third-party code relies on the fact that it is assigned in the constructor and never changes. 44 | 45 | ## Reference-level explanation 46 | [reference-level-explanation]: #reference-level-explanation 47 | 48 | The constructor of `Component` is updated to take one argument: `def __init__(self, signature=None)`. 49 | - If the `signature` argument is not provided, the signature is derived from class variable annotations, as in RFC 2, and assigned to an internal attribute. 50 | - If the `signature` argument is provided, it is type-checked/type-cast and assigned to the internal attribute. 51 | - If a `Signature` is provided as a value, it is used as-is. 52 | - If a `dict` is provided, it is cast by calling `wiring.Signature(signature)`. 53 | - No other types are accepted. 54 | 55 | If both the `signature` argument is provided and variable annotations are present, the constructor raises a `TypeError`. This is to guard against accidentally passing a `Signature` as an argument when constructing a component that is not parametric. (If this behavior is undesirable for some reason, it is always possible to implement a constructor that does not pass superclasses' constructor, and redefine the `signature` property, but this should be done as last resort only. We should cleary document that the `signature` property should return the exact same value at all times for any given `Component` instance.) 56 | 57 | The `signature` property is redefined to return the value of this internal attribute. 58 | 59 | No access to this attribute is provided besides the means above, and the name of the attribute is not defined in the documentation. 60 | 61 | After assigning the internal attribute, the constructor creates the members as in RFC 2. 62 | 63 | ## Drawbacks 64 | [drawbacks]: #drawbacks 65 | 66 | None. This properly enforces an invariant that is already relied upon. 67 | 68 | ## Rationale and alternatives 69 | [rationale-and-alternatives]: #rationale-and-alternatives 70 | 71 | Some other behaviors were considered for `Component`: those which would made `signature` a class attribute, assigned in `__init_subclass__`. However, this option would leave the `signature` attribute mutable (as class-level properties are not reasonably supported in Python), which is undesirable, and significantly complicated the case of signatures with interior mutability. 72 | 73 | ## Unresolved questions 74 | [unresolved-questions]: #unresolved-questions 75 | 76 | None. 77 | 78 | ## Future possibilities 79 | [future-possibilities]: #future-possibilities 80 | 81 | None. 82 | -------------------------------------------------------------------------------- /text/0039-empty-case.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-01-08 2 | - RFC PR: [amaranth-lang/rfcs#39](https://github.com/amaranth-lang/rfcs/pull/39) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1021](https://github.com/amaranth-lang/amaranth/issues/1021) 4 | 5 | # Change semantics of no-argument `m.Case()` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Change the semantics of `with m.Case():` (without any arguments) from always-true conditional to always-false conditional. Likewise, change `value.matches()` from returning `C(1)` to returning `C(0)`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Currently, `with m.Case():` results in an always-true conditional, and `value.matches()` likewise returns a const-1 value. However, this is not consistent with what would be expected from extrapolating the non-empty case. 16 | 17 | In all non-empty cases, the semantics are equivalent to an OR of equality comparisons with all specified values: 18 | 19 | `value.matches(1, 2, 3) =def= (value == 1) | (value == 2) | (value == 3)` 20 | 21 | `value.matches(1, 2, 3) =def= Const(0) | (value == 1) | (value == 2) | (value == 3)` 22 | 23 | Extrapolating from this, one would expect `value.matches()` to be the empty OR, ie. `Const(0)`. 24 | 25 | It is unlikely that any manually written code will rely on this, but this can be a dangerous trap for machine-generated code that doesn't take the empty case into account. 26 | 27 | ## Guide-level explanation 28 | [guide-level-explanation]: #guide-level-explanation 29 | 30 | The semantics of `m.Case()` change from always matching to never matching. Likewise, the semantics of `value.matches()` change from always-1 to always-0. The change is committed to the current `main` branch and will be included in Amaranth 0.5. 31 | 32 | Amaranth 0.4.1 is released with the old semantics, but a deprecation warning is emitted whenever `m.Case()` or `value.matches()` is used. 33 | 34 | ## Reference-level explanation 35 | [reference-level-explanation]: #reference-level-explanation 36 | 37 | See above. 38 | 39 | ## Drawbacks 40 | [drawbacks]: #drawbacks 41 | 42 | Obviously backwards-incompatible, changes the semantics of a language construct to the direct opposite. 43 | 44 | ## Rationale and alternatives 45 | [rationale-and-alternatives]: #rationale-and-alternatives 46 | 47 | It is unlikely anyone actually uses `value.matches()` directly, since this is just a constant. For generated code, the current semantics is much more likely to be a bug that intended behavior. 48 | 49 | For `m.Case()` the situation is similar: it is redundant with `m.Default()`, which should be used instead. It is somewhat possible that there is code out there written by someone who didn't know about `m.Default()` and ended up using `m.Case()` instead (the official documentation didn't include either for a long time). This code will need to be fixed. 50 | 51 | An alternative, if the empty case is deemed too confusing or insufficiently useful, is to make the semantics a hard error instead. 52 | 53 | ## Prior art 54 | [prior-art]: #prior-art 55 | 56 | The current behavior is likely taken directly from RTLIL, which exhibits a similar inconsistency. 57 | 58 | ## Unresolved questions 59 | [unresolved-questions]: #unresolved-questions 60 | 61 | Should we include more warnings about the change? This RFC proposes a warning in the 0.4.1 release, but this will never be seen by someone always using amaranth from git main. 62 | 63 | ## Future possibilities 64 | [future-possibilities]: #future-possibilities 65 | 66 | None. 67 | -------------------------------------------------------------------------------- /text/0040-arbitrary-memory-shape.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-01-22 2 | - RFC PR: [amaranth-lang/rfcs#40](https://github.com/amaranth-lang/rfcs/pull/40) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1048](https://github.com/amaranth-lang/amaranth/issues/1048) 4 | 5 | # Arbitrary `Memory` shapes 6 | 7 | > **Obsoleted by** 8 | > This RFC is obsoleted by [RFC 45](0045-lib-memory.md). 9 | 10 | ## Summary 11 | [summary]: #summary 12 | 13 | Extend `Memory` to support arbitrary element shapes. 14 | 15 | ## Motivation 16 | [motivation]: #motivation 17 | 18 | `Memory` currently only supports plain unsigned elements, with the width set by the `width` argument. 19 | Extending this to allow arbitrary shapes eliminates the need for manual conversion when used to store signed data and value-castables. 20 | 21 | ## Guide-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | The `width` argument to `Memory()` is replaced with `shape`, accepting anything that is `ShapeLike`. 25 | Since a plain bit width is `ShapeLike`, this is a direct superset of existing functionality. 26 | 27 | If `shape` is shape-castable, each element passed to the `init` argument is passed through `shape.const()`. 28 | 29 | Example: 30 | ```python 31 | RGB = StructLayout({"r": 8, "g": 8, "b": 8}) 32 | 33 | palette = Memory(shape = RGB, depth = 16, init = [ 34 | {"r": 0, "g": 0, "b": 0}, 35 | {"r": 255, "g": 0, "b": 0}, 36 | # ... 37 | ]) 38 | ``` 39 | 40 | ## Reference-level explanation 41 | [reference-level-explanation]: #reference-level-explanation 42 | 43 | `Memory.__init__()` gets a new `shape` argument, accepting any `ShapeLike`. 44 | 45 | The `width` argument to `Memory.__init__()` deprecated and removed in a later Amaranth version. Passing both `width` and `shape` is an error. 46 | 47 | The `Memory.shape` attribute is added. 48 | 49 | The `Memory.width` attribute is made a read-only wrapper for `Shape.cast(self.shape).width`. 50 | 51 | The `Memory.depth` attribute is made read-only. 52 | 53 | `ReadPort.data` and `WritePort.data` are updated to be `Signal(memory.shape)`. 54 | 55 | `WritePort.__init__()` raises an exception if `granularity` is specified and `shape` is not an unsigned `Shape`. 56 | 57 | `DummyPort.__init__()` gets a new `data_shape` argument. `data_width` is deprecated and removed in a later Amaranth version. 58 | 59 | ## Drawbacks 60 | [drawbacks]: #drawbacks 61 | 62 | Churn. 63 | 64 | ## Rationale and alternatives 65 | [rationale-and-alternatives]: #rationale-and-alternatives 66 | 67 | - This could also be accomplished by adding a wrapper around `Memory`. 68 | - A wrapper would result in more code to maintain than simply updating `Memory`, since both the memory object itself and the port objects would have to be wrapped. 69 | 70 | ## Prior art 71 | [prior-art]: #prior-art 72 | 73 | Being able to make a `Memory` with an arbitrary element shape is analogous to being able to make an array with an arbitrary element type in any high level programming language. 74 | 75 | ## Unresolved questions 76 | [unresolved-questions]: #unresolved-questions 77 | 78 | None. 79 | 80 | ## Future possibilities 81 | [future-possibilities]: #future-possibilities 82 | 83 | - Once `Memory` is extended to support arbitrary shapes, it is natural that higher level constructs building on `Memory` like FIFOs gets the same treatment. 84 | 85 | - `granularity` could later be allowed to be used with other kinds of shapes. 86 | - This is desirable for e.g. `lib.data.ArrayLayout`, but is not currently possible since `Memory` lives in `hdl.mem`, and `hdl` can't depend on `lib`. 87 | -------------------------------------------------------------------------------- /text/0042-const-from-shape-castable.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-02-05 2 | - RFC PR: [amaranth-lang/rfcs#42](https://github.com/amaranth-lang/rfcs/pull/42) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1084](https://github.com/amaranth-lang/amaranth/issues/1084) 4 | 5 | # `Const` from shape-castable 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Allow passing a shape-castable to `Const`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | We currently have two incompatible syntaxes for making a constant, depending on whether it's made from a `Shape` or a shape-castable. 16 | The former uses `Const(value, shape)`, while the latter requires `shape.const(value)`. 17 | 18 | Making `Const` accept shape-castables means we'll have a syntax that works for all shape-likes, reducing the need to special case for shape-castables. 19 | 20 | ## Guide- and reference-level explanation 21 | [guide-level-explanation]: #guide-level-explanation 22 | 23 | `Const(value, shape)` checks whether `shape` is a shape-castable and returns `shape.const(value)` when this is the case. 24 | 25 | ## Drawbacks 26 | [drawbacks]: #drawbacks 27 | 28 | - A `Const()` constructor sometimes returning non-`Const` objects can be confusing. 29 | - `Signal()` already behaves this way. 30 | 31 | ## Rationale and alternatives 32 | [rationale-and-alternatives]: #rationale-and-alternatives 33 | 34 | - This is consistent with how `Signal()` handles shape-castables. 35 | 36 | Alternatives: 37 | - Do not do this. Require code that makes constants from a passed shape-like to check whether it got passed a shape-castable or not and pick the appropriate syntax. 38 | 39 | ## Prior art 40 | [prior-art]: #prior-art 41 | 42 | [RFC #15](0015-lifting-shape-castables.md) added the equivalent behavior to `Signal()`. 43 | 44 | ## Unresolved questions 45 | [unresolved-questions]: #unresolved-questions 46 | 47 | None. 48 | 49 | ## Future possibilities 50 | [future-possibilities]: #future-possibilities 51 | 52 | None. 53 | -------------------------------------------------------------------------------- /text/0043-rename-reset-to-init.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-02-12 2 | - RFC PR: [amaranth-lang/rfcs#43](https://github.com/amaranth-lang/rfcs/pull/43) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1110](https://github.com/amaranth-lang/amaranth/issues/1110) 4 | 5 | # Rename `reset=` to `init=` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Rename the `reset=` keyword argument to `init=` in `Signal()`, `In()`, `Out()`, `Memory()`, etc. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | The value specified by the `reset=` keyword argument is called an "initial value" in our language guide, never a "reset value", because when the signal is driven combinatorially, it does not get *reset* to that value (as it holds no state), but rather *initialized* to that value whenever the value of the signal is computed. 16 | 17 | Calling it a "reset value" (even implicitly, by the name of the keyword argument) makes teaching Amaranth more difficult and is a point of confusion. All of our documentation already has to carefully avoid calling it a "reset value", and similarly, any Amaranth experts would have to avoid that in speech. Tutorial authors [have to call it out explicitly](https://github.com/RobertBaruch/amaranth-tutorial/blob/6a7ebe9cb3b904177df876df01552099e89c031f/3_modules.md#resetdefault-values-for-signals). 18 | 19 | `Memory` already does not have a `reset=` argument or accessor; it uses `init=`. `Memory` should be consistent with `Signal`. 20 | 21 | ## Guide-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | All instances of `reset=` keyword argument in Amaranth are changed to use `init=`. `reset_less=`, `async_reset=`, etc remain as they are. Using `reset=` raises a deprecation warning but continues working for a long time, perhaps Amaranth 1.0. 25 | 26 | ## Reference-level explanation 27 | [reference-level-explanation]: #reference-level-explanation 28 | 29 | The following entry points have their `reset=` argument and attribute changed to `init=`: 30 | - `Signal(reset=)` 31 | - `Signal.like(reset=)` 32 | - `with m.FSM(reset=):` 33 | - `FFSynchronizer(reset=)` 34 | - `Member(reset=)` (which handles `In(reset=)`, `Out(reset=)`) 35 | 36 | Specifically: 37 | - At most one of `init=` and `reset=` keyword arguments are accepted. Using `reset=` prints a deprecation warning. The semantics is exactly the same. 38 | - Wherever there was an accessible `.reset` attribute, a getter and a setter are provided that read/write `.init`. 39 | - No specific deprecation timeline is established, unlike with many other features. We could do this, perhaps, in two years, or by Amaranth 1.0. 40 | 41 | ## Drawbacks 42 | [drawbacks]: #drawbacks 43 | 44 | Churn. 45 | 46 | ## Rationale and alternatives 47 | [rationale-and-alternatives]: #rationale-and-alternatives 48 | 49 | The primary alternative is to not do this. Amaranth is steadily gaining popularity, so the earlier we do it the better. 50 | 51 | There are no good alternatives to the `init=` name, especially given our already written documentation and its use for `Memory`. 52 | 53 | ## Prior art 54 | [prior-art]: #prior-art 55 | 56 | Verilog has `initial x = 1;`, though that does not result in a reset being inferred. 57 | 58 | ## Unresolved questions 59 | [unresolved-questions]: #unresolved-questions 60 | 61 | When exactly do we remove `reset=`? It seems valuable to do it as late as possible to minimize breakage of lightly maintained Amaranth code. 62 | 63 | ## Future possibilities 64 | [future-possibilities]: #future-possibilities 65 | 66 | None. -------------------------------------------------------------------------------- /text/0046-shape-range-1.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-02-05 2 | - RFC PR: [amaranth-lang/rfcs#46](https://github.com/amaranth-lang/rfcs/pull/46) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1081](https://github.com/amaranth-lang/amaranth/issues/1081) 4 | 5 | # Change `Shape.cast(range(1))` to `unsigned(0)` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Change `Shape.cast(range(1))` to return `unsigned(0)` instead of `unsigned(1)`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | 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)`. 16 | 17 | 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`. 18 | 19 | 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. 20 | 21 | ## Guide-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | `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))`. 25 | 26 | 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. 27 | 28 | ## Reference-level explanation 29 | [reference-level-explanation]: #reference-level-explanation 30 | 31 | See above. 32 | 33 | ## Drawbacks 34 | [drawbacks]: #drawbacks 35 | 36 | This is a minor backwards compatibility hazard. 37 | 38 | ## Rationale and alternatives 39 | [rationale-and-alternatives]: #rationale-and-alternatives 40 | 41 | The change itself is simple enough that it cannot really be done any other way. 42 | 43 | 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. 44 | 45 | ## Prior art 46 | [prior-art]: #prior-art 47 | 48 | None. 49 | 50 | ## Unresolved questions 51 | [unresolved-questions]: #unresolved-questions 52 | 53 | None. 54 | 55 | ## Future possibilities 56 | [future-possibilities]: #future-possibilities 57 | 58 | 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. 59 | -------------------------------------------------------------------------------- /text/0049-soc-gpio-peripheral.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-08 2 | - RFC PR: [amaranth-lang/rfcs#49](https://github.com/amaranth-lang/rfcs/pull/49) 3 | - Amaranth Issue: [amaranth-lang/amaranth-soc#77](https://github.com/amaranth-lang/amaranth-soc/issues/77) 4 | 5 | # GPIO peripheral RFC 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Add a SoC peripheral to control GPIO pins. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | [GPIOs](https://en.wikipedia.org/wiki/General-purpose_input/output) are useful for a wide range of scenarios, such as driving external circuitry or acting as fallback for unimplemented/misbehaving peripherals in early iterations of a design. 16 | 17 | Amaranth SoC seems like an appropriate place for a GPIO peripheral, which depends on features that are already provided by the library. Due to its relative simplicity, it is also a good candidate for using the recent CSR register API in realistic conditions. 18 | 19 | ## Guide-level explanation 20 | [guide-level-explanation]: #guide-level-explanation 21 | 22 | ### Usage 23 | 24 | ```python3 25 | from amaranth import * 26 | from amaranth.lib import wiring 27 | from amaranth.lib.wiring import connect 28 | 29 | from amaranth_soc import csr 30 | from amaranth_soc import gpio 31 | 32 | 33 | class MySoC(wiring.Component): 34 | def elaborate(self, platform): 35 | m = Module() 36 | 37 | # ... 38 | 39 | # Use a GPIO peripheral to control four LEDs: 40 | 41 | m.submodules.led_gpio = led_gpio = gpio.Peripheral(pin_count=4, addr_width=8, data_width=8) 42 | 43 | for n in range(4): 44 | connect(m, led_gpio.pins[n], platform.request("led", n, dir="io")) 45 | 46 | # Add the peripheral to a CSR bus decoder: 47 | 48 | m.submodules.csr_decoder = csr_decoder = csr.Decoder(addr_width=31, data_width=8) 49 | 50 | csr_decoder.add(led_gpio.bus, addr=0x1000) 51 | 52 | # ... 53 | 54 | return m 55 | ``` 56 | 57 | ### Overview 58 | 59 | The following figure is a simplified diagram of the peripheral. CSR registers are on the left-hand side, a single pin is on the right side: 60 | 61 | 62 | 63 | ### Registers 64 | 65 | #### Mode (read/write) 66 | 67 | bf([
 69 |          {name: 'pin_0', bits: 2, attr: 'RW'},
 70 |          {name: 'pin_1', bits: 2, attr: 'RW'},
 71 |          {name: 'pin_2', bits: 2, attr: 'RW'},
 72 |          {name: 'pin_3', bits: 2, attr: 'RW'},
 73 |      ], {bits: 8}) 74 | 75 | Each `Mode.pin_x` field can hold the following values: 76 | 77 | ```python3 78 | class Mode(enum.Enum, shape=unsigned(2)): 79 | INPUT_ONLY = 0b00 80 | PUSH_PULL = 0b01 81 | OPEN_DRAIN = 0b10 82 | ALTERNATE = 0b11 83 | ``` 84 | 85 | Each `Mode.pin_x` field resets to `INPUT_ONLY`. 86 | 87 | If `Mode.pin_x` is `INPUT_ONLY`: 88 | - `pins[x].oe` is 0. 89 | - `pins[x].o` is connected to `Output.pin_x`. 90 | - `Input.pin_x` is connected to `pins[x].i`. 91 | - `alt_mode[x]` is 0. 92 | 93 | If `Mode.pin_x` is `PUSH_PULL`: 94 | - `pins[x].oe` is 1. 95 | - `pins[x].o` is conencted to `Output.pin_x`. 96 | - `Input.pin_x` is connected to `pins[x].i`. 97 | - `alt_mode[x]` is 0. 98 | 99 | If `Mode.pin_x` is `OPEN_DRAIN`: 100 | - `pins[x].oe` is connected to `~Output.pin_x`. 101 | - `pins[x].o` is 0. 102 | - `Input.pin_x` is connected to `pins[x].i`. 103 | - `alt_mode[x]` is 0. 104 | 105 | If `Mode.pin_x` is `ALTERNATE`: 106 | - `pins[x].oe` is 0. 107 | - `pins[x].o` is connected to `Output.pin_x`. 108 | - `Input.pin_x` is connected to `pins[x].i`. 109 | - `alt_mode[x]` is 1. 110 | 111 | When `alt_mode[x]` is 1, a component connected to the GPIO peripheral (such as a pin multiplexer) may assign implementation-specific functions to `Input.pin_x` and `Output.pin_x`. 112 | 113 | #### Input (read-only) 114 | 115 | bf([
117 |          {name: 'pin_0', bits: 1, attr: 'R'},
118 |          {name: 'pin_1', bits: 1, attr: 'R'},
119 |          {name: 'pin_2', bits: 1, attr: 'R'},
120 |          {name: 'pin_3', bits: 1, attr: 'R'},
121 |      ], {bits: 4}) 122 | 123 | The number of synchronization stages between `pins[x].i` and `Input.pin_x` is defined by the `input_stages` parameter, which defaults to 2. Synchronization is done on rising edges of `ClockSignal("sync")`. 124 | 125 | #### Output (read/write) 126 | 127 | bf([
129 |          {name: 'pin_0', bits: 1, attr: 'RW'},
130 |          {name: 'pin_1', bits: 1, attr: 'RW'},
131 |          {name: 'pin_2', bits: 1, attr: 'RW'},
132 |          {name: 'pin_3', bits: 1, attr: 'RW'},
133 |      ], {bits: 4}) 134 | 135 | Each `Output.pin_x` field resets to 0. 136 | 137 | #### SetClr (write-only) 138 | 139 | bf([
141 |          {name: 'pin_0', bits: 2, attr: 'W'},
142 |          {name: 'pin_1', bits: 2, attr: 'W'},
143 |          {name: 'pin_2', bits: 2, attr: 'W'},
144 |          {name: 'pin_3', bits: 2, attr: 'W'},
145 |      ], {bits: 8}) 146 | 147 | - Writing `0b01` to `SetClr.pin_x` sets `Output.pin_x`. 148 | - Writing `0b10` to `SetClr.pin_x` clears `Output.pin_x`. 149 | - Writing `0b00` or `0b11` to `SetClr.pin_x` has no effect. 150 | 151 | ## Reference-level explanation 152 | [reference-level-explanation]: #reference-level-explanation 153 | 154 | ### `amaranth_soc.gpio.PinSignature` 155 | 156 | The `gpio.PinSignature` class is a `wiring.Signature` describing the interface between the GPIO peripheral and a single pin. 157 | 158 | The members of a `gpio.PinSignature` are defined as follows: 159 | 160 | ```python3 161 | { 162 | "i": In(unsigned(1)), 163 | "o": Out(unsigned(1)), 164 | "oe": Out(unsigned(1)), 165 | } 166 | ``` 167 | 168 | ### `amaranth_soc.gpio.Peripheral` 169 | 170 | The `gpio.Peripheral` class is a `wiring.Component` implementing a GPIO controller, with: 171 | - a `.__init__(self, *, pin_count, addr_width, data_width, name=None, input_stages=2)` constructor, where: 172 | * `pin_count` is a non-negative integer. 173 | * `input_stages` is a non-negative integer. 174 | * `addr_width`, `data_width` and `name` are passed to a `csr.Builder` 175 | - a `.signature` property, that returns a `wiring.Signature` with the following members: 176 | 177 | ```python3 178 | { 179 | "bus": In(csr.Signature(addr_width, data_width)), 180 | "pins": Out(gpio.PinSignature()).array(pin_count), 181 | "alt_mode": Out(unsigned(pin_count)), 182 | } 183 | ``` 184 | 185 | - a `.elaborate(self, platform)` method, that connects each pin in `self.pins` to its associated fields in the registers exposed by `self.bus`. 186 | 187 | ## Drawbacks 188 | [drawbacks]: #drawbacks 189 | 190 | While existing implementations (such as STM32 GPIOs) have features like pin multiplexing and configurable pull-up/down resistors, in the proposed design, those would have to be implemented in a separate component. 191 | 192 | ## Rationale and alternatives 193 | [rationale-and-alternatives]: #rationale-and-alternatives 194 | 195 | The proposed design moves platform-specific details outside of its scope, which: 196 | - reduces the amount of non-portable code to maintain, while allowing implementation freedom for users needing it. 197 | - avoids introducing dependencies on upstream APIs that are deprecated or expected to evolve soon (such as `amaranth.build`). 198 | 199 | As an alternative: 200 | - do not host any peripheral in amaranth-soc and always develop them downstream. 201 | - include a pin multiplexer inside the GPIO peripheral. 202 | 203 | ## Prior art 204 | [prior-art]: #prior-art 205 | 206 | While they can be found in most microcontollers, the design of GPIOs in STM32 has inspired part of this RFC. 207 | 208 | ## Unresolved questions 209 | [unresolved-questions]: #unresolved-questions 210 | 211 | - ~~Should we support synchronizing a pin input on falling edges of the clock ?~~ (@whitequark) Users can synchronize pin inputs on falling edges by instantiating a `gpio.Peripheral` with `input_stages=0`, and providing their own synchronization mechanism. 212 | 213 | - What is our policy for backward-compatible extensions of the peripheral ? (@whitequark) If or when we add registers for new optional features, such as pull-ups, switchable schmitt triggers, switchable output driver strengths, etc, each register will always reside at the same fixed (for a given pin count) address regardless of which features are enabled, and each of these registers will be all-0s after reset, where such all-0s value will provide behavior identical to the behavior of the peripheral without the optional feature. Slots in the address space will never be reallocated with a different meaning once allocated upstream in Amaranth SoC. 214 | * This will be important to industry users caring about forward and cross-family/cross-configuration compatibility. 215 | * In a perfect world this would be our policy for every peripheral. Realistically, we'll only be able to provide this strongest guarantee for only a subset of peripherals. 216 | 217 | ## Future possibilities 218 | [future-possibilities]: #future-possibilities 219 | 220 | - Implement a pin multiplexer peripheral, that can be composed with this one to allow reusing other pins of a SoC as GPIOs. 221 | - Add support for interrupts. 222 | 223 | ## Acknowledgements 224 | 225 | @whitequark and @tpwrules provided valuable feedback while this RFC was being drafted. 226 | -------------------------------------------------------------------------------- /text/0049-soc-gpio-peripheral/reg-input.svg: -------------------------------------------------------------------------------- 1 | 0123pin_0pin_1pin_2pin_3RRRR 2 | -------------------------------------------------------------------------------- /text/0049-soc-gpio-peripheral/reg-mode.svg: -------------------------------------------------------------------------------- 1 | 01234567pin_0pin_1pin_2pin_3RWRWRWRW 2 | -------------------------------------------------------------------------------- /text/0049-soc-gpio-peripheral/reg-output.svg: -------------------------------------------------------------------------------- 1 | 0123pin_0pin_1pin_2pin_3RWRWRWRW 2 | -------------------------------------------------------------------------------- /text/0049-soc-gpio-peripheral/reg-setclr.svg: -------------------------------------------------------------------------------- 1 | 01234567pin_0pin_1pin_2pin_3WWWW 2 | -------------------------------------------------------------------------------- /text/0051-const-from-bits.md: -------------------------------------------------------------------------------- 1 | - Start Date: (fill me in with today's date, 2024-03-04) 2 | - RFC PR: [amaranth-lang/rfcs#51](https://github.com/amaranth-lang/rfcs/pull/51) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1187](https://github.com/amaranth-lang/amaranth/issues/1187) 4 | 5 | # Add `ShapeCastable.from_bits` and `amaranth.lib.data.Const` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | A new user-definable method is added to `ShapeCastable`: `from_bits(bits: int)`. It is used to convert constants into user-defined types, for example when a user-typed value is read in simulation. 11 | 12 | A new type is added: `amaranth.lib.data.Const`. It is used as the return type of `Layout.from_bits`. `Layout.const` is also changed to return it instead of `View`. 13 | 14 | ## Motivation 15 | [motivation]: #motivation 16 | 17 | The current Amaranth model provides a useful set of primitives for manipulating opaque signals and values of custom types, via `ShapeCastable` and `ValueCastable`. However, there is little support for manipulating constants of custom types. 18 | 19 | This is reasonable, as Amaranth mostly deals with elaborating a design hierarchy that operates on symbolic values that won't have actual values assigned before the circuit is operational. However, this need comes up in the Amaranth simulator, where the values have to be actually materialized. 20 | 21 | Specifically, the simulator requires two operations to usefully support `ValueCastable` expressions: 22 | 23 | 1. Conversion from user-facing constant value to raw bits, used when a `ValueCastable` experession is set to a value, or a cell of `ShapeCastable`-typed `Memory` is written by a user process. This need is already covered by the existing `ShapeCastable.const`. 24 | 2. Conversion from raw bits to user-facing constant value, used when a `ValueCastable` expression is requested, or a cell of `ShapeCastable`-type `Memory` is read by a user process. This need will be covered by the proposed `from_bits` method. 25 | 26 | For `amaranth.lib.enum`, the implementation of `from_bits` is trival: the value is looked up in the enum, and the enum value is returned. However, we have no good type to return from `from_bits` for `amaranth.lib.data`. Thus, a new `lib.data.Const` class is proposed. 27 | 28 | 29 | ## Guide-level explanation 30 | [guide-level-explanation]: #guide-level-explanation 31 | 32 | A new method is added to `ShapeCastable`: `from_bits(bits: int)`, which, given a constant, interprets the constant as a value of the given `ShapeCastable` and returns an arbitrary Python value. The returned value must be round-trippable back to the original constant via `ShapeCastable.const`. This method will be used whenever there is a need to translate a raw bit pattern through a `ShapeCastable`, such as when reading a `ShapeCastable`-typed value in simulation. 33 | 34 | ``` 35 | >>> class Abc(amaranth.lib.enum.Enum, shape=unsigned(2)): 36 | ... X = 0 37 | ... Y = 1 38 | ... Z = 2 39 | ... 40 | >>> Abc.from_bits(2) 41 | 42 | ``` 43 | 44 | To support this functionality in `amaranth.lib.data`, a new `lib.data.Const` class is added, which wraps a constant. Like `View`, this class provides attribute-based and indexing-based access to `Layout` fields. However, when a field is accessed, `lib.data.Const` returns a raw int value (for plain `Shape`-typed fields), or calls `from_bits` on the underlying bit pattern (for `ShapeCastable` fields). The class is also `ValueCastable`. 45 | 46 | ``` 47 | >>> class Def(amaranth.lib.data.Struct): 48 | ... a: Abc 49 | ... b: unsigned(2) 50 | ... 51 | >>> a = Def.from_bits(9) 52 | >>> a 53 | Const(StructLayout({'a': , 'b': unsigned(2)}), 9) 54 | >>> a.a 55 | 56 | >>> a.b 57 | 2 58 | ``` 59 | 60 | Since it is a better fit, the return type of `Layout.const` is changed to `lib.data.Const`. 61 | 62 | ## Reference-level explanation 63 | [reference-level-explanation]: #reference-level-explanation 64 | 65 | A new method is added on `ShapeCastable`: `from_bits(bits: int)`. The default implementation raises an error. A warning is raised when this method is not overriden in a subclass. The warning will become a hard error in Amaranth 0.6. 66 | 67 | The argument to the method must be an `int` that is a valid constant value of the `ShapeCastable`'s underlying shape. 68 | 69 | The method must return something acceptable to the `const` method on the same `ShapeCastable`, and the value must round-trip, that is: 70 | 71 | ``` 72 | # assume `bits` is an int that is a valid const of `shape_castable.as_shape()` 73 | value = shape_castable.const(shape_castable.from_bits(bits)).as_value() 74 | assert isinstance(value, Const) 75 | assert value.value == bits 76 | ``` 77 | 78 | It is recommended, but not strictly required, that the returned value is a `ValueLike`. 79 | 80 | The `sim.get` method in the upcoming RFC 36 will call this method when called on a `ValueCastable` with a custom shape. The `sim.set` method will likewise call `ShapeCastable.const`. 81 | 82 | A `from_bits` implementation is added onto `amaranth.lib.enum.EnumMeta`. If the given bit pattern has a corresponding enum value, the enum value is returned. Otherwise, the argument is returned unchanged. 83 | 84 | A new class is added: 85 | 86 | - `amaranth.lib.data.Const(layout, bits: int)`: bits must be a valid constant of the underlying shape; derives from `ValueCastable` 87 | - `shape()`: returns the underlying shape 88 | - `as_value()`: returns `Const(bits, len(layout))` 89 | - `__getitem__`: 90 | - resolves the field like `View.__getitem__` 91 | - extracts the corresponding field from `bits` 92 | - if the field is a `ShapeCastable`, returns the result of calling its `from_bits` with the extracted value 93 | - otherwise, returns the raw field value as an `int` 94 | - `__getattr__`: resolves the field like `View.__getattr__`, then proceeds as above 95 | - `__eq__` and `__ne__`: 96 | - if the other value is a `lib.data.Const` with the same layout, compares the underlying `bits` and returns a Python `bool` 97 | - if the other value is a `View` with the same layout, delegates to `Value.__eq__` (and thus returns a 1-bit `Value`) 98 | - otherwise, raises `TypeError` 99 | - all other operators: raises `TypeError` (to prevent the default `Value` operator implementations) 100 | 101 | A `from_bits` implementation is added to `Layout`, `Struct`, and `Union`. It returns a `lib.data.Const` instance. 102 | 103 | `Layout.const`, `Struct.const`, and `Union.const` are changed to return a `lib.data.Const` of matching layout. They are also changed to accept such constants if given, and return them unchanged. 104 | 105 | ## Drawbacks 106 | [drawbacks]: #drawbacks 107 | 108 | Adds an extra hook onto `ShapeCastable` that everyone must implement. However, it is not clear how this could be avoided. 109 | 110 | When a `Struct` (or `Union`) is involved, `isinstance(sim.get(my_struct_signal), MyStruct)` will be false. This could be avoided by horrifying metaclass hacks, but it's unclear if it's a good idea. 111 | 112 | The change to `Layout.const` etc is not fully backwards-compatible. It is believed that this is not much of an issue, since its main user is `Signal` `init=` argument, for which it doesn't matter. 113 | 114 | ## Rationale and alternatives 115 | [rationale-and-alternatives]: #rationale-and-alternatives 116 | 117 | The proposed design of `from_bits` is essentially the minimal interface needed to support the needs of simulation. Additionally, it is designed so that it can be called essentially recursively by aggregate `ShapeCastable`s, such as `Layout`. 118 | 119 | An alternative (proposed originally by RFC 36) would be to let `ValueCastable` implement `get`/`set` methods for simulation. However, this has obvious drawbacks: 120 | 121 | - two methods needed per `ShapeCastable` 122 | - doesn't work for reading or writing memories, as they don't have underlying signals 123 | - when the value is an aggregate, there's no obvious way to delegate to construct the values of fields 124 | 125 | The `lib.data.Const` design closely follows `View`. An alternative would be for `from_bits` to return a list or dict of fields (recursively processed by `from_bits`), paralleling the accepted argument format of `const`. However, this results in inferior user experience. 126 | 127 | `lib.data.Const` conceivably could be made mutable, providing `__setattr__` / `__setitem__` in the future. However, Amaranth tries to avoid mutable data types, and it was felt that keeping it explicitly immutable was the best way forward. Its main use is believed to be simulation, and in simulation one can just call `sim.set()` on the `View` field directly if needed. Outside of simulation, we can add a `.like()`-like constructor for `lib.data.Const` to modify some fields of the aggregate. 128 | 129 | ## Prior art 130 | [prior-art]: #prior-art 131 | 132 | The author believes user-defined types usually come in three parts: 133 | 134 | - a `ShapeCastable`, ie. the type itself 135 | - a `ValueCastable` proxying arbitrary `Value`s 136 | - a class representing constant values of the type 137 | 138 | For enums, these correspond to `EnumMeta`, `EnumView`, and the `Enum` itself. For plain values, they correspond to `Shape`, `Value`, and `int`. The proposed RFC 41 likewise includes `fixed.Shape`, `fixed.Value`, and `fixed.Const`. 139 | 140 | The first half of this RFC provides a generic way to construct the third part from a bit pattern. The second half of this RFC fills the missing third part for `amaranth.lib.data`. 141 | 142 | ## Unresolved questions 143 | [unresolved-questions]: #unresolved-questions 144 | 145 | None. 146 | 147 | ## Future possibilities 148 | [future-possibilities]: #future-possibilities 149 | 150 | The `lib.data.Const` class can be expanded in functionality: 151 | 152 | - alternative constructors could be added, to let user create and manipulate const struct-typed values 153 | -------------------------------------------------------------------------------- /text/0052-choice.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-07-01 2 | - RFC PR: [amaranth-lang/rfcs#52](https://github.com/amaranth-lang/rfcs/pull/52) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1445](https://github.com/amaranth-lang/amaranth/issues/1445) 4 | 5 | # Add `amaranth.hdl.Choice`, a pattern-based `Value` multiplexer 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | A new type of expression is added: `amaranth.hdl.Choice`. It is essentially a variant of `m.Switch` 11 | that returns a `Value` using the same patterns as `m.Case` for selection. 12 | 13 | ## Motivation 14 | [motivation]: #motivation 15 | 16 | We currently have several multiplexer primitives: 17 | 18 | - `Mux`, selecting from two values 19 | - `Array` indexing, selecting from multiple values by a simple index 20 | - `.bit_select` and `.word_select`, selecting from slices of a single value by a simple index 21 | - `m.Switch` together with combinatorial assignment to an intermediate `Signal`, selecting from multiple values by pattern matching 22 | 23 | It is, however, not possible to select from multiple values by pattern matching without using an intermediate `Signal` and assignment (which can be a problem in contexts where a `Module` is not available). This RFC aims to close this hole. 24 | 25 | This feature is generally useful and has been on the roadmap for a while. The immediate impulse for writing this RFC was using this functionality to implement string formatting for `lib.enum` values. 26 | 27 | ## Guide-level explanation 28 | [guide-level-explanation]: #guide-level-explanation 29 | 30 | The `Choice` expression can be used to select from among several values via pattern matching: 31 | 32 | ```py 33 | abc = Signal(8) 34 | a = Signal(8) 35 | b = Signal(8) 36 | sel = Signal(4) 37 | m.d.comb += abc.eq(Choice(sel) 38 | # any pattern or tuple of patterns usable in `Value.matches` or `m.Case` is valid as key 39 | .case(1, a) 40 | .case(2, b) 41 | .case((3, 4), a + b) 42 | .case("11--", a - b) 43 | .case(("10--", "011-"), a * b) 44 | .default(13) 45 | ) 46 | ``` 47 | 48 | is equivalent to writing: 49 | 50 | ```py 51 | with m.Switch(sel): 52 | with m.Case(1): 53 | m.d.comb += abc.eq(a) 54 | with m.Case(2): 55 | m.d.comb += abc.eq(b) 56 | with m.Case(3, 4): 57 | m.d.comb += abc.eq(a + b) 58 | with m.Case("11--"): 59 | m.d.comb += abc.eq(a - b) 60 | with m.Case("10--", "011-"): 61 | m.d.comb += abc.eq(a * b) 62 | with m.Default(): 63 | m.d.comb += abc.eq(13) 64 | ``` 65 | 66 | `Choice` can also be used on the left-hand side of an assignment: 67 | 68 | ```py 69 | a = Signal(8) 70 | b = Signal(8) 71 | c = Signal(8) 72 | d = Signal(8) 73 | sel = Signal(2) 74 | m.d.sync += (Choice(sel) 75 | .case(0, a) 76 | .case(1, b) 77 | .case(2, c) 78 | .default(d) 79 | .eq(0)) 80 | ``` 81 | 82 | which is equivalent to: 83 | 84 | ```py 85 | with m.Switch(sel): 86 | with m.Case(0): 87 | m.d.sync += a.eq(0) 88 | with m.Case(1): 89 | m.d.sync += b.eq(0) 90 | with m.Case(2): 91 | m.d.sync += c.eq(0) 92 | with m.Default(): 93 | m.d.sync += d.eq(0) 94 | ``` 95 | 96 | If `default=` is not used, the default value is 0 when on right-hand side, and no assignment happens when on left-hand side. 97 | 98 | In addition, `Mux` becomes assignable if the second and third argument are both assignable. 99 | 100 | ## Reference-level explanation 101 | [reference-level-explanation]: #reference-level-explanation 102 | 103 | A new expression type is added: 104 | 105 | - `amaranth.hdl.Choice(sel: ValueLike)`: creates a new `Choice` expression with no cases 106 | - `.case(self, patterns: int | str | tuple[int | str], value: ValueLike) -> Choice`: creates a new `Choice` based on this one, adding anoter case to it 107 | - `.default(self, value: ValueLike) -> Choice`: creates a new `Choice` based on this one, adding a default case to it 108 | 109 | The expression evaluates `sel`, then matches it to `patterns` of every `.case()` in turn. If a match is found, the expression evaluates to the corresponding `value` of the first found match. If no match is found, the expression evaluates to the `value` of `.default()`, or to `Cat()` with no arguments if no `.default()` was used. The expression is assignable if all `.case()` values and `.default()` value (if any) are assignable. 110 | 111 | Neither `.case()` nor `.default()` can be called on a `Choice` that already has a `.default()`. 112 | 113 | The shape of the expression is determined as follows: 114 | 115 | - if all `value` arguments are `ShapeCastable`, and it is the same `ShapeCastable` for all of them (as determined by `__eq__` on the `ShapeCastable`), the resulting value is transformed through `ShapeCastable.__call__` of that shape-castable 116 | - if all `value` arguments have a plain `Shape`, the minimum shape that can represent the shapes of all `cases` values and `default` (ie. determined the same as for `Array` proxy or `Mux` tree). 117 | - otherwise, an exception is raised 118 | 119 | The default when `.default()` is not specified is `Cat()` to ensure the correct semantics for assignment (ie. discarding the assigned value). This also happens to provide the default 0 when on right-hand side. 120 | 121 | `Choice` is also added to the Amaranth prelude. 122 | 123 | In addition, the existing `Mux` expression is made valid on the left-hand side of an assignment, as if it was lowered as follows: 124 | 125 | ```py 126 | def Mux(sel, val1, val0): 127 | return Choice(sel).case(0, val0).default(val1) 128 | ``` 129 | 130 | `ArrayProxy` (ie. the type currently returned by `Array` indexing) is changed from a native `Value` to a `ValueCastable` that lowers to `Choice` (removing the odd case where we can currently build an invaid `Value`). To avoid problems with lowering the out-of-bounds case, the value returned for out-of-bounds `Array` accesses is changed to 0. 131 | 132 | `__eq__` is added to the `ShapeCastable` protocol and documented (we already have suitable implementations in `lib.data` and `lib.enum`). 133 | 134 | ## Drawbacks 135 | [drawbacks]: #drawbacks 136 | 137 | The language gets slightly more complex. 138 | 139 | ## Rationale and alternatives 140 | [rationale-and-alternatives]: #rationale-and-alternatives 141 | 142 | The core functionality is fairly obvious. However, the syntax is not. Other possibilities include: 143 | 144 | - `*args` (or perhaps iterable) of `(key, value)` tuples: 145 | 146 | ```py 147 | Choices(sel, 148 | (1, a), 149 | (2, b), 150 | ((3, 4), c), 151 | ("11--", d), 152 | default=e 153 | ) 154 | ``` 155 | 156 | - *args of newly-defined `amaranth.hdl.Case` object (not to be confused with `m.Case`): 157 | 158 | ```py 159 | Choices(sel, 160 | Case(1, a), 161 | Case(2, b), 162 | Case((3, 4), c), 163 | Case("11--", d), 164 | default=e, 165 | ) 166 | ``` 167 | 168 | The syntax proposed has been selected to have extension space (in the form of keyword arguments) for e.g. optional guard conditions. 169 | 170 | ## Prior art 171 | [prior-art]: #prior-art 172 | 173 | This feature is inspired by Rust `match` construct. 174 | 175 | ## Unresolved questions 176 | [unresolved-questions]: #unresolved-questions 177 | 178 | The name is subject to bikeshed. An obvious alternative is `Match`, though this RFC avoids using this name, as it suggests much more advanced pattern matching (with variable capture) than is currently available. 179 | 180 | ## Future possibilities 181 | [future-possibilities]: #future-possibilities 182 | 183 | Optional guard conditions could be added to `Choice` and `m.Switch` cases (like Rust's `if` guards on `match` branches). 184 | -------------------------------------------------------------------------------- /text/0054-read-port-init.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-18 2 | - RFC PR: [amaranth-lang/rfcs#54](https://github.com/amaranth-lang/rfcs/pull/54) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1212](https://github.com/amaranth-lang/amaranth/issues/1212) 4 | 5 | # Initial and reset values on memory read ports 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Synchronous memory read ports get initial values, just like `Signal`s. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Currently, the initial state of synchronous memory read port's data output is undefined in synthesis, making it one of the very few places in Amaranth where an undefined value can be obtained, and an unexpected one at that. This also cannot be caught with pysim, as it initializes all read ports to 0. This is easily fixable on almost all FPGA targets, as almost all FPGAs have well-defined initial values. 16 | 17 | Further, the read ports on almost all FPGA targets also support a reset signal, which is currently not exposed in any way in Amaranth. 18 | 19 | The hardware capabilities for yosys-supported targets are as follows: 20 | 21 | - Xilinx BRAM: arbitrary initial and reset values, reset can be sync or async (except for very old FPGAs that have sync reset only) 22 | - Lattice, Anlogic, Gowin BRAM; Xilinx URAM, Nexus LRAM: always-zero initial and reset value, reset can be sync or async 23 | - Efinix, iCE40, Gatemate BRAM; iCE40 SPRAM: undefined initial value, no reset 24 | - LUT RAM on any target: full support (uses a normal FF to create a sync read port) 25 | 26 | Additionally, on any platform where requested initial value or reset is not natively supported by hardware, yosys will insert a FF and a mux to make it work regardless. 27 | 28 | This RFC thus proposes to: 29 | 30 | - close the expressiveness hole, making use of the hardware features where supported, using emulation otherwise 31 | - get rid of the undefined behavior 32 | 33 | 34 | ## Guide-level explanation 35 | [guide-level-explanation]: #guide-level-explanation 36 | 37 | Synchronous memory read ports behave in most respects like `Signal`s driven from a synchronous clock domain. As such, they have an initial value that can be set via `init=` on the constructor: 38 | 39 | ```py 40 | mem = Memory(shape=unsigned(8), depth=8, init=[]) 41 | rp = mem.read_port(domain="sync", init=13) 42 | ``` 43 | 44 | The read port's `data` signal will hold the initial value at startup and whenever a domain reset occurs. Additionally, as for `Signal`, `reset_less=True` can be specified to make the port not react to the domain reset. 45 | 46 | ## Reference-level explanation 47 | [reference-level-explanation]: #reference-level-explanation 48 | 49 | `lib.memory.Memory.read_port` and `lib.memory.ReadPort` get two new keyword-only arguments: `init=None` and `reset_less=False`. For synchronous read ports, they effectively have the same behavior as they have on `Signal` when applied to `port.data`. They become introspectable attributes on the port. If the port has `comb` domain, they cannot be changed from their default values and are meaningless. 50 | 51 | `hdl.MemoryInstance.read_port` likewise gets two new keyword-only arguments: `init=0` and `reset_less=False`. `init` must be an integer. 52 | 53 | ## Drawbacks 54 | [drawbacks]: #drawbacks 55 | 56 | This is not natively supported by *all* FPGAs. While it can be reasonably cheaply emulated, in the author's experience, any amount of emulation circuitry inserted automatically by the toolchain to ensure well-defined behavior results solely in complaints. 57 | 58 | ## Rationale and alternatives 59 | [rationale-and-alternatives]: #rationale-and-alternatives 60 | 61 | An alternative is to put `init` and `reset_less` on the signature and on the `port.data` signal instead of on the read port. However, `reset_less` is currently not supported by `lib.wiring`, and `is_compliant` will reject any signal with `reset_less=True`. This could be changed by a simple amendment to RFC 2. 62 | 63 | ## Prior art 64 | [prior-art]: #prior-art 65 | 66 | None, or rather obvious enough. 67 | 68 | ## Unresolved questions 69 | [unresolved-questions]: #unresolved-questions 70 | 71 | None. 72 | 73 | ## Future possibilities 74 | [future-possibilities]: #future-possibilities 75 | 76 | A way to explicitly request undefined initial value could be added in the future, once undefined values are a well-defined concept in Amaranth. 77 | -------------------------------------------------------------------------------- /text/0056-mem-wide.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-18 2 | - RFC PR: [amaranth-lang/rfcs#56](https://github.com/amaranth-lang/rfcs/pull/56) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1211](https://github.com/amaranth-lang/amaranth/issues/1211) 4 | 5 | # Asymmetric memory port width 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Memory read and write ports can have varying width, allowing eg. for memories with 8-bit read path and 32-bit write path. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | This is a common hardware feature. It allows for eg. having a slow but wide port in one domain, and fast but narrow port in another domain. On platforms lacking dedicated hardware support, it can often be emulated almost for free. 16 | 17 | 18 | ## Guide-level explanation 19 | [guide-level-explanation]: #guide-level-explanation 20 | 21 | Memories can have asymmetric port width. To use that feature, instantiate the memory with the shape of the narrowest desired port, then pass the `aggregate` argument on ports that should be wider than that: 22 | 23 | ```py 24 | m.submodules.mem = mem = Memory(shape=unsigned(8), depth=4096, init=[]) 25 | # 8-bit write port 26 | wp = mem.write_port() 27 | # 32-bit read port 28 | rp = mem.read_port(aggregate=4) 29 | # Address 0x123 on rp is equivalent to addresses (0x123 * 4, 0x123 * 4 + 1, 0x123 * 4 + 2, 0x123 + 3) on wp. 30 | # Shape of rp.data is ArrayLayout(unsigned(8), 4) 31 | ``` 32 | 33 | ## Reference-level explanation 34 | [reference-level-explanation]: #reference-level-explanation 35 | 36 | Both `lib.memory.Memory.read_port` and `lib.memory.Memory.write_port` have a new `aggregate=None` keyword-only argument. If `aggregate` is not `None`, the behavior is as follows: 37 | 38 | - `aggregate` has to be a power of two 39 | - `mem.depth` must be divisible by `aggregate` 40 | - the `shape` passed to the `*Port.Signature` constructor becomes `ArrayLayout(memory.shape, aggregate)` 41 | - implied by the previous point, `granularity` on wide write ports is counted in terms of single memory row 42 | - the `addr_width` passed to `*Port.Signature` constructor becomes `ceil_log2(memory.depth // aggregate)` 43 | 44 | The behavior of wide ports is defined by expanding them to `aggregate` narrow ports: 45 | 46 | - the `data` of subport `i` is connected to `data[i]` of wide port 47 | - the `addr` of subport `i` is connected to `addr * aggregate + i` of wide port 48 | - for read ports and write ports without granularity, `en` is broadcast 49 | - for write ports with granularity, `en` of subport `i` is connected to `en[i // granularity]` of wide port 50 | 51 | No change is made to signature types or port types. Wide ports are recognized solely by their relation to `memory.shape`. 52 | 53 | The rules for `MemoryInstance.read_port` and `MemoryInstance.write_port` change as follows: 54 | 55 | - define `aggregate_log2 = ceil_log2(depth) - len(addr)`, `aggregate = 1 << aggregate_log2` 56 | - `aggregate_log2` must be non-negative 57 | - `depth` must be divisible by `aggregate` 58 | - `len(data)` must be equal to `width * aggregate` 59 | - for write ports, one of the following must hold: 60 | - `aggregate` is divisible by `len(en)` 61 | - `len(en)` is divisible by `aggregate` and `len(data)` is divisible by `len(en)` 62 | 63 | ## Drawbacks 64 | [drawbacks]: #drawbacks 65 | 66 | More complexity. 67 | 68 | Wide write ports with sub-row write granularity cannot be expressed. However, there is no hardware that would actually natively support such a combination. 69 | 70 | ## Rationale and alternatives 71 | [rationale-and-alternatives]: #rationale-and-alternatives 72 | 73 | The design is straightforward enough. 74 | 75 | An alternative is not doing this. Yosys already has an optimization pass that recognizes wide ports from a collection of narrow ports, so this is not necessarily an expressiveness hole. However, platforms with non-yosys toolchain could still benefit from custom lowering for this case. 76 | 77 | ## Prior art 78 | [prior-art]: #prior-art 79 | 80 | This proposal is directly based on yosys memory model. 81 | 82 | ## Unresolved questions 83 | [unresolved-questions]: #unresolved-questions 84 | 85 | None. 86 | 87 | ## Future possibilities 88 | [future-possibilities]: #future-possibilities 89 | 90 | Similar functionality could potentially be added to `lib.fifo`. 91 | -------------------------------------------------------------------------------- /text/0057-single-field-shortcut.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-15 2 | - RFC PR: [amaranth-lang/rfcs#57](https://github.com/amaranth-lang/rfcs/pull/57) 3 | - Amaranth SoC Issue: [amaranth-lang/amaranth-soc#78](https://github.com/amaranth-lang/amaranth-soc/issues/78) 4 | 5 | # Single-Field Register Definition and Usage Shortcut 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Add a shortcut to `csr.Register` to enable easier and cleaner definition and use 11 | of registers which contain exactly one `csr.Field`. 12 | 13 | ## Motivation 14 | [motivation]: #motivation 15 | 16 | Currently, use of a `csr.Register` in an amaranth-soc peripheral requires creation of a map (or list) of `csr.Field`s, which actually represent the register data and control access to each part. This is commonly done by creating a `Register` subclass. Each `Field` also needs a name (or index) in the `Register`. However, many registers in peripherals only contain one field, so the extra name and subclass are unnecessary and redundant. This RFC introduces a shortcut for constructing and using a `Register` containing exactly one un-named `Field`. 17 | 18 | ## Guide-level explanation 19 | [guide-level-explanation]: #guide-level-explanation 20 | 21 | Consider the following simple peripheral which generates a toggling signal on bits of an output corresponding to bits set in a register: 22 | 23 | ```python 24 | from amaranth import * 25 | from amaranth.lib import wiring 26 | from amaranth.lib.wiring import In, Out 27 | from amaranth_soc import csr 28 | 29 | class Toggle(wiring.Component): 30 | bus: In(csr.Signature(addr_width=1, data_width=32)) 31 | toggle: Out(32) 32 | 33 | class Toggle(csr.Register, access="rw"): 34 | toggle: csr.Field(csr.action.RW, 32) 35 | 36 | def __init__(self): 37 | self._toggle = Toggle() 38 | 39 | # ... bridge setup ... 40 | 41 | def elaborate(self, platform): 42 | m = Module() 43 | 44 | # ... bridge setup ... 45 | 46 | for i in range(32): 47 | with m.If(self._toggle.f.toggle.data[i]): 48 | m.d.sync += self.toggle[i].eq(~self.toggle[i]) 49 | with m.Else(): 50 | m.d.sync += self.toggle[i].eq(0) 51 | 52 | return m 53 | ``` 54 | 55 | The `toggle` name is used, among other ways, to name the register class, to name 56 | the field within, and to access that field on the register instance during 57 | elaboration. 58 | 59 | This can be simplified by passing the `csr.Field` directly to the `csr.Register` 60 | without enclosing it in a dict/list and naming/indexing it, or subclassing 61 | `csr.Register`. This also causes the `.f` member to access that Field directly 62 | instead of needing to provide a name/index. 63 | 64 | This simplifies the design as follows, reducing clutter and redundancy: 65 | 66 | ```python 67 | from amaranth import * 68 | from amaranth.lib import wiring 69 | from amaranth.lib.wiring import In, Out 70 | from amaranth_soc import csr 71 | 72 | class Toggle(wiring.Component): 73 | bus: In(csr.Signature(addr_width=1, data_width=32)) 74 | toggle: Out(32) 75 | 76 | def __init__(self): 77 | # Register can take the Field directly, eliminating the subclass 78 | self._toggle = csr.Register(csr.Field(csr.action.RW, 32), access="rw") 79 | 80 | # ... bridge setup ... 81 | 82 | def elaborate(self, platform): 83 | m = Module() 84 | 85 | # ... bridge setup ... 86 | 87 | for i in range(32): 88 | # FieldAction accessed directly, no need to specify `toggle` again 89 | with m.If(self._toggle.f.data[i]): 90 | m.d.sync += self.toggle[i].eq(~self.toggle[i]) 91 | with m.Else(): 92 | m.d.sync += self.toggle[i].eq(0) 93 | 94 | return m 95 | ``` 96 | 97 | ## Reference-level explanation 98 | [reference-level-explanation]: #reference-level-explanation 99 | 100 | (with reference to [RFC #16](https://github.com/amaranth-lang/rfcs/blob/main/text/0016-soc-csr-regs.md)) 101 | 102 | #### `csr.reg.Register` 103 | 104 | The `csr.reg.Register` class describes a CSR register `Component`, with: 105 | - a `.__init__(self, fields=None, access=None)` constructor, where: 106 | * `fields` is either: 107 | - a `dict` that will be instantiated as a `FieldActionMap`; 108 | - a `list` that will be instantiated as a `FieldActionArray`; 109 | - a `Field` that will be instantiated as a `FieldAction`; 110 | - `None`; in this case a `FieldActionMap` is instantiated from `Field` objects in variable annotations. 111 | * `access` is either a `csr.Element.Access` value, or `None`. 112 | - a `.field` property, returning the instantiated `FieldActionMap`/`FieldActionArray`/`FieldAction`; 113 | - a `.f` property, as a shorthand to `self.field`; 114 | - a `.__iter__(self)` method that yields, for each contained field, a tuple containing its path (as a tuple of names or indices) and its instantiated `FieldAction`. If only a single `Field` was supplied, its path is always `tuple()`. 115 | 116 | ## Drawbacks 117 | [drawbacks]: #drawbacks 118 | 119 | * A novice might not understand that it's possible to create a `Register` with multiple `Fields` if exposed only to code which uses single-`Field` `Register`s. 120 | * There is a difference between a `Register` with one named/indexed `Field` and with one unnamed `Field`. 121 | * Library code will be made more confusing by having `fields` refer to possibly only one field. 122 | * Downstream code will have to deal with a third type of object returned by the 123 | `.f` property which behaves unlike the current two. 124 | * BSP/doc generators will have to intelligently render a `Field` with an empty name and path (likely by omitting them, possibly by reusing the register's name). 125 | * An empty name is prohibited by `FieldActionMap`, so empty names are a novel 126 | problem. 127 | * The register does not know its own name independently of its container, so this may be difficult in practice. 128 | * Adding a second `Field` to a register created this way will break all accesses to the `.f` property and likely all BSP users (though the actual bus transactions to the first `Field` will not change). 129 | * This could be especially entertaining if the new `Field` names happen to 130 | be the same as properties of the first `Field`'s `FieldAction`. 131 | * BSP users will have to change all constants/functions named after just the 132 | register to instead use the register + the field name. 133 | 134 | ## Rationale and alternatives 135 | [rationale-and-alternatives]: #rationale-and-alternatives 136 | 137 | * Simple and easily explainable change, easy to migrate code to, though no 138 | migration is even necessary 139 | * Minimal change to the library's existing code 140 | * Nicely reduces redundant names, types, and properties 141 | * Not strictly necessary, not having it will just make peripheral code a little 142 | more cluttered 143 | * Users could instead be taught to eliminate the subclass by passing a dict to 144 | the `Register` constructor and assign the `.f` property of interest to a 145 | local variable to de-clutter code 146 | 147 | ## Prior art 148 | [prior-art]: #prior-art 149 | 150 | AVR microcontrollers have many registers where the field name is the same as the register name, though this is not always the case for single-field registers. Application code usually uses the register name in this case and ignores the field name. 151 | 152 | STM32 microcontrollers are similar, but the field name is usually a suffix of the register name, as the field name is not expected to be globally unique. However, the generated BSP files do appear to always contain the field name at least somewhere. 153 | 154 | Field arrays are already in some sense anonymous, being named by an integer instead of a string. 155 | 156 | More experienced input is welcomed here. 157 | 158 | ## Unresolved questions 159 | [unresolved-questions]: #unresolved-questions 160 | 161 | * ~~Should we change `fields` to `field` in the constructor and properties? This could break existing code.~~ 162 | * The property will be `.field` and the constructor will be `fields=`. 163 | * ~~Should we add `field` too? This would access the same object/s as `fields` and so would make available the option to use the right plurality, though it would be more code and wouldn't force it.~~ 164 | * The property will be `.field` and the constructor will be `fields=`. 165 | * ~~Should we force a `Register` with a single `Field` to always have that one be un-named? Would create backwards compatibility problems, but reduce ambiguity.~~ 166 | * No, would cause backwards compatibility problems, which we don't want. 167 | * ~~Should we go further? `Register` could take a single `FieldAction` and automatically wrap it in a `Field` (and inherit its access mode).~~ 168 | * No, unnecessarily convenient and not yet proved necessary by experience. 169 | 170 | ## Future possibilities 171 | [future-possibilities]: #future-possibilities 172 | 173 | None obvious, this is a pretty small and self-contained idea. 174 | -------------------------------------------------------------------------------- /text/0058-valuecastable-format.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-25 2 | - RFC PR: [amaranth-lang/rfcs#58](https://github.com/amaranth-lang/rfcs/pull/58) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1243](https://github.com/amaranth-lang/amaranth/issues/1243) 4 | 5 | # Core support for `ValueCastable` formatting 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | `Format` hooks are added, allowing custom formatting to be implemented for `ValueCastable`s. 11 | 12 | This RFC is only about adding hook support for custom `ShapeCastable`s. Providing actual formatting implementation for `lib.data` and `lib.enum` is left for a future RFC. 13 | 14 | ## Motivation 15 | [motivation]: #motivation 16 | 17 | Custom types, like enums and data structures, will not make immediate sense to the user when printed as raw bit patterns. However, it is often desirable to print them out with the `Print` statement. Thus, an extension point for custom formatting needs to be added to the `Format` machinery. 18 | 19 | ## Guide-level explanation 20 | [guide-level-explanation]: #guide-level-explanation 21 | 22 | `ShapeCastable` subtypes can define their own ways of formatting values by implementing the `format` hook: 23 | 24 | ```py 25 | class FixedPoint(ShapeCastable): 26 | ... 27 | 28 | def format(self, value, format_desc): 29 | if format_desc == "b": 30 | # Format a fixed-point value as binary. 31 | return Format("{value.int:b}.{value.fract:0{fract_bits}b}}", value=value, fract_bits=len(value.fract)) 32 | elif format_desc == "x": 33 | # Format a fixed-point value as hexadecimal (very simplified implementation). 34 | assert len(value.fract) % 4 == 0 35 | return Format("{value.int:x}.{value.fract:0{fract_digits}x}}", value=value, fract_digits=len(value.fract) // 4) 36 | else: 37 | ... 38 | 39 | # A fixed-point number with 8 integer and 8 fractional bits. 40 | num = Signal(FixedPoint(8, 8)) 41 | m.d.comb += [ 42 | num.eq(0x1234) 43 | Print(Format("Value in binary: {:b}", num)), 44 | Print(Format("Value in hexadecimal: {:x}", num)), 45 | ] 46 | ``` 47 | 48 | prints: 49 | 50 | ``` 51 | Value in binary: 00010010.00110100 52 | Value in hexadecimal: 12.34 53 | ``` 54 | 55 | However, sometimes it is also useful to print the raw value of such a signal. A shorthand is provided for that purpose: 56 | 57 | ```py 58 | m.d.comb += Print(Format("Value: {num:x} (raw: {num!v:x})", num=num)) 59 | ``` 60 | 61 | ``` 62 | Value: 12.34 (raw: 1234) 63 | ``` 64 | 65 | ## Reference-level explanation 66 | [reference-level-explanation]: #reference-level-explanation 67 | 68 | A new overridable method is added to `ShapeCastable`: 69 | 70 | - `format(self, value: ValueCastable, format_desc: str) -> Format` 71 | 72 | When a `ValueCastable` is formatted without specifying a conversion (ie. `"!r"` or `"!v"`): 73 | 74 | - `shape()` is called 75 | - if the shape is a `ShapeCastable` and has a `format` method, the value-castable being formatted and the format descriptor (the part after `:` in `{}`, if any) are passed directly to `shape.format()`, and the result (which must be a `Format` object) is included directly in place of the format specifier 76 | - otherwise (the shape is a plain `Shape`, or doesn't have `format`), `Value.cast()` is called on the value-castable, and formatting proceeds as if it was a plain value 77 | 78 | A new conversion, `!v`, is added to `Format` (with syntax analogous to `!r`). When specified, the value being formatted is converted through `Value.cast` before proceeding with further formatting. It can be used to print the raw value of value-castables. It is a no-op when used with a plain `Value`. 79 | 80 | An implementation of `__format__` is added to the `ValueCastable` base class that always raises a `TypeError`, directing the user to `Format` instead (like the one that already exists on plain `Value`). 81 | 82 | ## Drawbacks 83 | [drawbacks]: #drawbacks 84 | 85 | A new reserved name, `format`, is added to `ShapeCastable`, which is intended to be a fairly slim interface. 86 | 87 | The `__format__` on `ValueCastable` is the first time we have a method with an actual implementation. 88 | 89 | ## Rationale and alternatives 90 | [rationale-and-alternatives]: #rationale-and-alternatives 91 | 92 | The `format` hook is added on `ShapeCastable` instead of `ValueCastable`. This ensures that `ValueCastable`s without a custom shape automatically get plain formatting. 93 | 94 | The default behavior proposed in this RFC ensures that a formatting implementation is always available, allowing generic code to print arbitrary values without worrying about an exception. Eg. something like `lib.fifo` could use debug prints when an element is added, automatically using rich formatting for shapes with `format`, while falling back to plain integers when rich formatting is not available. 95 | 96 | alternative default behaviors possible are: 97 | 98 | - raise `TypeError` (disallow formatting value-castables without explicitly implemented formatting) 99 | - no default, require every `ShapeCastable` to implement `format` (compatibility breaking) 100 | 101 | To avoid reserving a name and interfering with user code, `format` could be renamed to `_amaranth_format`. 102 | 103 | ## Prior art 104 | [prior-art]: #prior-art 105 | 106 | This RFC is modelled directly on the `__format__` extension point in Python. 107 | 108 | ## Unresolved questions 109 | [unresolved-questions]: #unresolved-questions 110 | 111 | Bikeshed: what should `!v` be called? Alternatives proposed: 112 | 113 | - `!v` (cast to value) 114 | - `!i` (print as integer) 115 | - `!n` (print as number) 116 | - `!R` (print raw) 117 | - `!l` (lower to a value) 118 | - `!av` (as value) 119 | 120 | ## Future possibilities 121 | [future-possibilities]: #future-possibilities 122 | 123 | Actual formatting will need to be implemented for `lib.data` and `lib.enum`. 124 | -------------------------------------------------------------------------------- /text/0059-no-domain-upwards-propagation.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-25 2 | - RFC PR: [amaranth-lang/rfcs#59](https://github.com/amaranth-lang/rfcs/pull/59) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1242](https://github.com/amaranth-lang/amaranth/issues/1242) 4 | 5 | # Get rid of upwards propagation of clock domains 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Upwards propagation of clock domains is removed, `local=True` effectively becomes the default. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Currently, a clock domain created anywhere in the hierarchy is, by default, propagated everywhere within that hierarchy. The other option is marking a clock domain with `local=True`, which restricts propagation to only downwards from the point of definition. 16 | 17 | The default behavior constitutes the ultimate implicit spooky action at a distance, making it hard to reason about clock domain origins in complex Amaranth design. It is also rarely used. This dangerous default should be changed to something more reasonable. 18 | 19 | ## Guide and reference-level explanation 20 | [explanation]: #explanation 21 | 22 | In version 0.5, upwards domain propagation becomes deprecated. Any use of a domain that wouldn't be valid without upwards propagation (ie. wouldn't be valid if the domain were `local=True`) triggers a deprecation warning. 23 | 24 | In version 0.6, upwards domain propagation is removed entirely. The `local` argument to `ClockDomain` constructor becomes redundant and is deprecated. 25 | 26 | In version 0.7, the `local` argument is removed. 27 | 28 | ## Drawbacks 29 | [drawbacks]: #drawbacks 30 | 31 | A little bit of churn for designs with clock generator modules. 32 | 33 | When propagating a domain upwards is actually desired (eg. for clock generator modules), a workaround is now necessary to route the domain to the toplevel. The simplest way is to set an eg. `cd_sync` attribute on the module in the constructor, then do `m.domains.sync = clkgen.cd_sync` in the top-level. This has the slight disadvantage that the clock generator module cannot decide on the attributes of the clock domain (clock polarity, reset asyncness) based on the platform. 34 | 35 | ## Rationale and alternatives 36 | [rationale-and-alternatives]: #rationale-and-alternatives 37 | 38 | The core proposal is straightforward. The main question is how to handle the usecases currently served by purposeful use of upwards domain propagation. There are three alternatives: 39 | 40 | 1. None. Clock generator modules must export their domains through some other existing Amaranth functionality. This is the option proposed above. 41 | 2. Keep the current mechanism as-is, but make it opt-in, such by requiring `ClockDomain(global=True)`. 42 | 3. Add a mechanism to explicitly import a domain from a submodule. A draft is proposed here, for discussion purposes: 43 | 44 | ```py 45 | m.submodules.clkgen = ClockGenerator(...) 46 | m.domains.sync = ImportedDomain("clkgen", "sync") # Imports a domain defined in the submodule "clkgen" named "sync". 47 | ``` 48 | 49 | We feel that 1. is the best option to take, as introducing a new mechanism when a full redesign of the system is pending is likely to just result in another thing that will have to be deprecated later. 50 | 51 | ## Prior art 52 | [prior-art]: #prior-art 53 | 54 | None. 55 | 56 | ## Unresolved questions 57 | [unresolved-questions]: #unresolved-questions 58 | 59 | None. 60 | 61 | ## Future possibilities 62 | [future-possibilities]: #future-possibilities 63 | 64 | The concept of "private" clock domains has been proposed: clock domains that don't propagate at all, including downwards. 65 | 66 | It's clear that clock domain overhaul with a larger scope needs to be done. This RFC is (among other things) intended to enable such overhaul, by removing a feature likely to make it infeasible. 67 | -------------------------------------------------------------------------------- /text/0062-memory-data.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-03-25 2 | - RFC PR: [amaranth-lang/rfcs#62](https://github.com/amaranth-lang/rfcs/pull/62) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1241](https://github.com/amaranth-lang/amaranth/issues/1241) 4 | 5 | # The `MemoryData` class 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | A new class, `amaranth.hdl.MemoryData`, is added to represent the data and identity of a memory. It is used to reference the memory in simulation. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | It is commonly useful to access a memory in a simulation testbench without having to create a special port for it. This requires storing some kind of a reference to the memory on the elaboratables. 16 | 17 | Currently, the object used for this is of a private `MemoryIdentity` class. It is implicitly created by `lib.memory.Memory` constructor, and passed to the private `_MemorySim{Read|Write}` objects when `__getitem__` is called. This has a few problems: 18 | 19 | - the `Memory` needs to be instantiated in the constructor of the containing elaboratable; combined with its mutability and the occasional need to defer memory port creation to `elaborate`, this results in elaboratables that break when elaborated more than once 20 | - `amaranth.sim` currently requires a gross hack to recognize `Memory` objects in `traces` 21 | - occasionally, it is useful to have `Signal`s that are not included in the design proper, but are used to communicate between simulator processes; it could be likewise useful with memories, but it cannot work with the current code (since the `MemoryIdentity` that the simulator gets doesn't have enough information to actually create backing storage for the memory) 22 | - `MemoryIdentity` nor `MemoryInstance` don't contain information about the element shape, which would require further wrappers on `lib.Memory` to perform shape conversion in post-RFC 36 world 23 | 24 | The proposed `MemoryData` class: 25 | 26 | - replaces `MemoryIdentity` and serves as the reference point for the simulator 27 | - encapsulates the memory's shape, depth, and initial value (so the simulator can use it to create backing storage) 28 | - in the common scenario, is created by the user in elaboratable constructor, stored as an attribute on the elaboratable, then passed to `Memory` constructor in `elaborate` 29 | 30 | ## Guide-level explanation 31 | [guide-level-explanation]: #guide-level-explanation 32 | 33 | If the memory is to be accessible in simulation, the code to create a memory changes from: 34 | 35 | ```py 36 | m.submodules.memory = memory = Memory(shape=..., depth=..., init=...) 37 | port = memory.read_port(...) 38 | ``` 39 | 40 | to: 41 | 42 | ```py 43 | # in __init__ 44 | self.mem_data = MemoryData(shape=..., depth=..., init=...) 45 | # in elaborate 46 | m.submodules.memory = memory = Memory(self.mem_data) 47 | port = memory.read_port(...) 48 | ``` 49 | 50 | The `my_component.mem_data` object can then be used in simulation to read and write memory: 51 | 52 | ```py 53 | addr = 0x1234 54 | row = sim.get(mem_data[addr]) 55 | row += 1 56 | sim.set(mem_data[addr], row) 57 | ``` 58 | 59 | The old way of creating memories is still supported, though somewhat less flexible. 60 | 61 | ## Reference-level explanation 62 | [reference-level-explanation]: #reference-level-explanation 63 | 64 | Two new classes are added: 65 | 66 | - `amaranth.hdl.MemoryData(*, shape: ShapeLike, depth: int, init: Iterable[int | Any], name=None)`: represents a memory's data storage. `name`, if not specified, defaults to the variable name used to store the `MemoryData`, like it does for `Signal`. 67 | - `__getitem__(self, addr: int) -> MemoryData._Row | ValueCastable`: creates a `MemoryData._Row` object; if `self.shape` is a `ShapeCastable`, the `MemoryData._Row` object constructed is immediately wrapped via `ShapeCastable.__call__` 68 | - `amaranth.hdl.MemoryData._Row` (subclass of `Value`): represents a single row of `MemoryData`, has no public constructor nor operations (other than ones derived from `Value`), can only be used in simulator processes and testbenches 69 | 70 | The `MemoryData` class allows access to its constructor arguments via read-only properties. 71 | 72 | The `lib.memory.Memory.Init` class is moved to `amaranth.hdl.MemoryData.Init`. It is used for the `init` property of `MemoryData`. 73 | 74 | The `Memory` constructor is changed to: 75 | 76 | - `amaranth.lib.memory.Memory(data: MemoryData = None, *, shape=None, depth=None, init=None, name=None)` 77 | 78 | - either `data`, or all three of `shape`, `depth`, `init` need to be provided, but not both 79 | - if `data` is provided, it is used directly, and stored 80 | - if `shape`, `depth`, `init` (and possibly `name`) are provided, they are used to create a `MemoryData`, which is then stored 81 | 82 | The `MemoryData` object is accessible via a new read-only `data` property on `Memory`. The existing `shape`, `depth`, `init` properties become aliases for `data.shape`, `data.depth`, `data.init`. 83 | 84 | `MemoryInstance` constructor is likewise changed to: 85 | 86 | - `amaranth.hdl.MemoryInstance(data: MemoryData, *, attrs={})` 87 | 88 | The `sim.memory_read` and `sim.memory_write` methods proposed by RFC 36 are removed. Instead, the new `Memory._Row` simulation-only value is introduced, which can be passed to `get`/`set`/`changed` like any other value. Masked writes can be implemented by `set` with a slice of `MemoryData._Row`, just like for signals. 89 | 90 | `MemoryData` and `MemoryData._Row` instances (possibly wrapped in `ShapeCastable` for the latter) can be added to the `traces` argument when writing a VCD file with the simulator. 91 | 92 | Using `MemoryData._Row` within an elaboratable results in an immediate error, even if the design is only to be used in simulation. The only place where `MemoryData._Row` is valid is within an argument to `sim.get`/`sim.set`/`sim.changed` and similar functions. 93 | 94 | `sim.edge` remains restricted to plain `Signal` and single-bit slices thereof. `MemoryData._Row` is not supported. 95 | 96 | ## Drawbacks 97 | [drawbacks]: #drawbacks 98 | 99 | None. 100 | 101 | ## Rationale and alternatives 102 | [rationale-and-alternatives]: #rationale-and-alternatives 103 | 104 | `MemoryData` having `shape`, `depth`, and `init` is necessary to allow the simulator to create the underlying storage if the memory is not included in the design hierarchy, but is used to communicate between simulator processes. 105 | 106 | ## Prior art 107 | [prior-art]: #prior-art 108 | 109 | `MemoryData` is conceptually equivalent to a 2D `Signal`, for simulation purposes. It thus follows similar rules. 110 | 111 | ## Unresolved questions 112 | [unresolved-questions]: #unresolved-questions 113 | 114 | None. 115 | 116 | ## Future possibilities 117 | [future-possibilities]: #future-possibilities 118 | 119 | A `MemoryData.Slice` class could be added, allowing a whole range of memory addresses to be `get`/`set` at once, monitored for changes with `changes`, or added to `traces`. 120 | 121 | Support for `__getitem__(Value)` could be added (currently it would be blocked on CXXRTL capabilities). 122 | -------------------------------------------------------------------------------- /text/0063-remove-lib-coding.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-04-08 2 | - RFC PR: [amaranth-lang/rfcs#63](https://github.com/amaranth-lang/rfcs/pull/63) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1292](https://github.com/amaranth-lang/amaranth/issues/1292) 4 | 5 | # Remove `amaranth.lib.coding` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Remove `amaranth.lib.coding` and all classes in it. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | This module has been essentially inherited from Migen and doesn't meet the bar for inclusion in Amaranth standard library. Most of the functionality included is so simple that it's essentially easier to just inline an implementation. The rest of it would be better served by a function than a module. 16 | 17 | ## Guide-level explanation 18 | [guide-level-explanation]: #guide-level-explanation 19 | 20 | The module `amaranth.lib.coding` and all classes in it are removed. To continue using it, copy the contents of the module into your own project. 21 | 22 | ## Reference-level explanation 23 | [reference-level-explanation]: #reference-level-explanation 24 | 25 | All classes within `amaranth.lib.coding` are deprecated in Amaranth 0.5 and removed (along with the module) in Amaranth 0.6. The Gray encoder is moved to `lib.fifo` as a private implementation detail. 26 | 27 | ## Drawbacks 28 | [drawbacks]: #drawbacks 29 | 30 | - Churn. 31 | 32 | ## Rationale and alternatives 33 | [rationale-and-alternatives]: #rationale-and-alternatives 34 | 35 | - This module is out of place in the standard library. 36 | - It has not seen much use and is trivially implemented outside of it. 37 | - Downstream consumers tend to inline the logic anyway. 38 | 39 | ## Unresolved questions 40 | [unresolved-questions]: #unresolved-questions 41 | 42 | None. 43 | 44 | ## Future possibilities 45 | [future-possibilities]: #future-possibilities 46 | 47 | The functionality could be brought back at a future point with a more suitable lightweight interface (as functions instead of modules), when the core language is flexible enough to support that. 48 | -------------------------------------------------------------------------------- /text/0065-format-struct-enum.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-04-08 2 | - RFC PR: [amaranth-lang/rfcs#65](https://github.com/amaranth-lang/rfcs/pull/65) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1293](https://github.com/amaranth-lang/amaranth/issues/1293) 4 | 5 | # Special formatting for structures and enums 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Extend `Format` machinery to support structured description of aggregate and enumerated data types. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | When a `lib.data.Layout`-typed signal is included in the design, it is often useful to treat it as multiple signals for debug purposes: 16 | 17 | - in pysim VCD output, it is useful to have each field as its own trace 18 | - in RTLIL output, it is useful to include a wire for each field (in particular, for CXXRTL simulation purposes) 19 | 20 | Likewise, `lib.enum.Enum`-typed signals benefit from being marked as enum-typed in RTLIL output. 21 | 22 | Currently, both of the above usecases are covered by the private `ShapeCastable._value_repr` interface. This interface has been added in a rush for the Amaranth 0.4 release as a stopgap measure, to ensure switching from `hdl.rec` to `lib.data` doesn't cause a functionality regression ([amaranth-lang/amaranth#790](https://github.com/amaranth-lang/amaranth/issues/790)). Such forbidden coupling between `lib` and `hdl` via private interfaces is frowned upon, and the time has come to atone for our sins and create a public interface that can be used for all sorts of aggregate and enumerated data types. 23 | 24 | Since both needs relate to presentation of shape-castables, we propose piggybacking the functionality on top of `ShapeCastable.format` machinery. At the same time, we propose implementing formatting for `lib.data` and `lib.enum`. 25 | 26 | ## Guide-level explanation 27 | [guide-level-explanation]: #guide-level-explanation 28 | 29 | Shape-castables implementing enumerated types can return a `Format.Enum` instance instead of a `Format` from their `format` method: 30 | 31 | ```py 32 | class MyEnum(ShapeCastable): 33 | ... 34 | 35 | def format(self, obj, spec): 36 | assert spec == "" 37 | return Format.Enum(Value.cast(obj), { 38 | 0: "ABC", 39 | 1: "DEF", 40 | 2: "GHI", 41 | }) 42 | ``` 43 | 44 | For formatting purposes, this is equivalent to: 45 | 46 | ```py 47 | class MyEnum(ShapeCastable): 48 | ... 49 | 50 | def format(self, obj, spec): 51 | assert spec == "" 52 | return Format( 53 | "{:s}", 54 | Mux(Value.cast(obj) == 0, int.from_bytes("ABC".encode(), "little"), 55 | Mux(Value.cast(obj) == 1, int.from_bytes("DEF".encode(), "little"), 56 | Mux(Value.cast(obj) == 2, int.from_bytes("GHI".encode(), "little"), 57 | int.from_bytes("[unknown]".encode(), "little") 58 | ))) 59 | ) 60 | ``` 61 | 62 | with the added benefit that any `MyEnum`-shaped signals will be automatically marked as enums in RTLIL output. 63 | 64 | Likewise, shape-castables implementing aggregate types can return a `Format.Struct` instance instead of a `Format`: 65 | 66 | ```py 67 | class MyStruct(ShapeCastable): 68 | ... 69 | 70 | def format(self, obj, spec): 71 | assert spec == "" 72 | return Format.Struct({ 73 | # Assume obj.a, obj.b, obj.c are accessors that return the struct fields as ValueLike. 74 | "a": Format("{}", obj.a), 75 | "b": Format("{}", obj.b), 76 | "c": Format("{}", obj.c), 77 | }) 78 | ``` 79 | 80 | For formatting purposes, this is equivalent to: 81 | 82 | ```py 83 | class MyStruct(ShapeCastable): 84 | ... 85 | 86 | def format(self, obj, spec): 87 | assert spec == "" 88 | return Format("{{a: {}, b: {}, c: {}}}", obj.a, obj.b, obj.c) 89 | ``` 90 | 91 | with the added benefit that any `MyStruct`-shaped signal will automatically have per-field traces included in VCD output and per-field wires included in RTLIL output. 92 | 93 | Implementations of `format` are added as appropriate to `lib.enum` and `lib.data`, making use of the above features. 94 | 95 | ## Reference-level explanation 96 | [reference-level-explanation]: #reference-level-explanation 97 | 98 | Three new classes are added: 99 | 100 | - `amaranth.hdl.Format.Enum(value: Value, /, variants: EnumType | dict[int, str])` 101 | - `amaranth.hdl.Format.Struct(value: Value, /, fields: dict[str, Format])` 102 | - `amaranth.hdl.Format.Array(value: Value, /, fields: list[Format])` 103 | 104 | Instances of these classes can be used wherever a `Format` can be used: 105 | 106 | - as an argument to `Print`, `Assert`, ... 107 | - included within another `Format` via substitution 108 | - returned from `ShapeCastable.format` 109 | - as a field format within `Format.Struct` or `Format.Array` 110 | 111 | When used for formatting: 112 | 113 | - `Format.Enum` will display as whichever string corresponds to current value of `value`, or as `"[unknown]"` otherwise. 114 | - `Format.Struct` will display as `"{a: , b: }"`. 115 | - `Format.Array` will display as `"[, ]"` 116 | 117 | Whenever a signal is created with a shape-castable as its shape, its `format` method is called with `spec=""` and the result is stashed away. 118 | 119 | VCD output is done as follows: 120 | 121 | 1. When a signal or field's format is just `Format("{}", value)`: value is emitted as a bitvector to VCD. 122 | 2. Otherwise, when a signal or field's custom format is not `Format.Struct` nor `Format.Array`: the format is evaluated every time the value changes, and the result is emitted as a string to VCD 123 | 3. When the custom format is `Format.Struct` or `Format.Array`: 124 | - the `value` as a whole is emitted as a bitvector 125 | - each field is emitted recursively (as a separate trace, perhaps with subtraces) 126 | 127 | RTLIL output is done as follows: 128 | 129 | 1. When a signal or field's format is a plain `Format` and contains exactly one format specifier: a wire is created and assigned with the specifier's value. 130 | 2. When a signal or field's format is a plain `Format` that doesn't conform to the above rule: no wire is created. 131 | 3. When a signal or field's format is a `Format.Enum`: a wire is created and assigned with the format's `value`. RTLIL attributes are emitted on it, describing the enumeration values. 132 | 4. When a signal or field's format is a `Format.Struct` or `Format.Array`: a wire is created and assigned with the format's `value`, representing the struct as a whole. For every field of the aggregate, the rules are applied recursively. 133 | 134 | ## Drawbacks 135 | [drawbacks]: #drawbacks 136 | 137 | More language complexity. 138 | 139 | A shape-castable using `Format.Struct` to mark itself as aggregate is forced to use the fixed `Format.Struct` display when formatted. 140 | 141 | ## Rationale and alternatives 142 | [rationale-and-alternatives]: #rationale-and-alternatives 143 | 144 | This design ties together concerns of formatting with structural description. An alternative would be to have separate hooks for those, like the current `_value_repr` interface. 145 | 146 | ## Prior art 147 | [prior-art]: #prior-art 148 | 149 | None. 150 | 151 | ## Unresolved questions 152 | [unresolved-questions]: #unresolved-questions 153 | 154 | None. 155 | 156 | ## Future possibilities 157 | [future-possibilities]: #future-possibilities 158 | 159 | The current `decoder` interface on `Signal` could be deprecated and retired. 160 | -------------------------------------------------------------------------------- /text/0066-simulation-time.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-07-29 2 | - RFC PR: [amaranth-lang/rfcs#66](https://github.com/amaranth-lang/rfcs/pull/66) 3 | - Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/1469) 4 | 5 | # Simulation time 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Add a type for representing time durations (including clock periods) and simulator methods to query the elapsed time. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | In [RFC #36](0036-async-testbench-functions.md), there were a plan to introduce a `.time()` method to query the current simulation time, but it got deferred due to the lack of a suitable return type. 16 | 17 | Internally simulation time is an integer number of femtoseconds, but exposing that directly is not ergonomic and making it a `float` of seconds sacrifices precision the longer a simulation runs. 18 | 19 | Also, to quote @whitequark: Time is not a number. 20 | 21 | ## Guide- and reference-level explanation 22 | [guide-level-explanation]: #guide-level-explanation 23 | 24 | A new type `Period` is introduced. 25 | It is exported from `amaranth.hdl` and also re-exported from `amaranth.sim`. 26 | It is immutable and has a constructor accepting at most one named argument, giving the following valid forms of construction: 27 | - `Period()` 28 | - Constructs a zero period. 29 | - `Period(s: numbers.Real)` 30 | - `Period(ms: numbers.Real)` 31 | - `Period(us: numbers.Real)` 32 | - `Period(ns: numbers.Real)` 33 | - `Period(ps: numbers.Real)` 34 | - `Period(fs: numbers.Real)` 35 | - The argument will be scaled according to its SI prefix and used to calculate the closest integer femtosecond representation. 36 | - `Period(Hz: numbers.Real)` 37 | - `Period(kHz: numbers.Real)` 38 | - `Period(MHz: numbers.Real)` 39 | - `Period(GHz: numbers.Real)` 40 | - The argument will be scaled according to its SI prefix and its reciprocal used to calculate the closest integer femtosecond representation. 41 | - A value of zero will raise `ZeroDivisionError`. 42 | - A negative value will raise `ValueError`. 43 | 44 | To convert it back to a number, the following properties are available: 45 | - `.seconds -> float` 46 | - `.milliseconds -> float` 47 | - `.microseconds -> float` 48 | - `.nanoseconds -> float` 49 | - `.picoseconds -> float` 50 | - `.femtoseconds -> int` 51 | 52 | To calculate the reciprocal frequency, the following properties are available: 53 | - `.hertz -> float` 54 | - `.kilohertz -> float` 55 | - `.megahertz -> float` 56 | - `.gigahertz -> float` 57 | - Accessing these properties when the period is zero will raise `ZeroDivisionError`. 58 | - Accessing these properties when the period is negative will raise `ValueError`. 59 | 60 | The following operators are defined: 61 | - `.__lt__(other: Period) -> bool` 62 | - `.__le__(other: Period) -> bool` 63 | - `.__eq__(other: Period) -> bool` 64 | - `.__ne__(other: Period) -> bool` 65 | - `.__gt__(other: Period) -> bool` 66 | - `.__ge__(other: Period) -> bool` 67 | - `.__hash__() -> int` 68 | - `.__bool__() -> bool` 69 | - `.__neg__() -> Period` 70 | - `.__pos__() -> Period` 71 | - `.__abs__() -> Period` 72 | - `.__add__(other: Period) -> Period` 73 | - `.__sub__(other: Period) -> Period` 74 | - `.__mul__(other: numbers.Real) -> Period` 75 | - `.__rmul__(other: numbers.Real) -> Period` 76 | - `.__truediv__(other: numbers.Real) -> Period` 77 | - `.__truediv__(other: Period) -> float` 78 | - `.__floordiv__(other: Period) -> int` 79 | - `.__mod__(other: Period) -> Period` 80 | - Operators on `Period`s are performed on the underlying femtosecond values. 81 | - Operators involving `numbers.Real` operands have the result rounded back to the closest integer femtosecond representation. 82 | - Operators given unsupported operand combinations will return `NotImplemented`. 83 | - `.__str__() -> str` 84 | - Equivalent to `.__format__("")` 85 | - `.__format__(format_spec: str) -> str` 86 | - The format specifier format is `[width][.precision][ ][unit]`. 87 | - An invalid format specifier raises `ValueError`. 88 | - If `width` is specified, the string is left-padded with space to at least the requested width. 89 | - If `precision` is specified, the requested number of decimal digits will be emitted. 90 | Otherwise, duration units will emit as many digits required for an exact result, while frequency units will defer to default `float` formatting. 91 | - If a space is present in the format specifier, the formatted string will have a space between the number and the unit. 92 | - `unit` can be specified as any of the argument names accepted by the constructor. 93 | If a unit is not specified, the largest duration unit that has a nonzero integer part is used. 94 | Formatting frequency units have the same restrictions and exception behavior as accessing frequency properties. 95 | 96 | `SimulatorContext` have an `.elapsed_time() -> Period` method added that returns the elapsed time since start of simulation. 97 | 98 | These methods that has a `period` argument currently taking seconds as a `float` are updated to take a `Period`: 99 | - `Simulator.add_clock()` 100 | - `SimulatorContext.delay()` 101 | 102 | These methods that has a `frequency` argument currently taking Hz as a `float` are updated to take a `Period`: 103 | - `ResourceManager.add_clock_constraint()` 104 | - `Clock.__init__()` 105 | 106 | The ability to pass seconds or Hz directly as a `float` is deprecated and removed in a future Amaranth version. 107 | 108 | The `Clock.period` property is updated to return a `Period` and the `Clock.frequency` property is deprecated. 109 | 110 | Consequently, `Platform.default_clk_frequency()` is also deprecated and replaced with a new method `Platform.default_clk_period() -> Period`. 111 | 112 | 113 | ## Drawbacks 114 | [drawbacks]: #drawbacks 115 | 116 | - Deprecating being able to pass seconds or Hz directly as a `float` creates churn. 117 | 118 | ## Rationale and alternatives 119 | [rationale-and-alternatives]: #rationale-and-alternatives 120 | 121 | - `.add_clock()` is usually passed the reciprocal of a frequency, so being able to construct a `Period` from a frequency instead of a duration is useful. 122 | - We could add a separate `Frequency` type, but in practice it would likely almost exclusively be used to calculate the reciprocal `Period`. 123 | 124 | - Accepting a `numbers.Real` when we're converting from a number lets us pass in any standard suitable number type. 125 | 126 | - The supported set of operators are the ones that have meaningful and useful semantics: 127 | - Comparing, adding and subtracting time periods makes sense. 128 | - Multiplying a time period with or dividing it by a real number scales the time period. 129 | - Dividing time periods gives the ratio between them. 130 | - Modulo of time periods is the remainder and thus still a time period. 131 | Potentially useful to calculate the phase offset between a timestamp and a clock period. 132 | - Reflected operators that only accept `Period` operands are redundant and omitted. 133 | 134 | - `.elapsed_time()` is only available from within a testbench/process, where it is well defined. 135 | 136 | - Instead of named constructor arguments, we could use a classmethod for each SI prefix. 137 | This was proposed in an earlier revision of this RFC. 138 | 139 | - Instead of returning `float`, we could return `fractions.Fraction` to ensure no precision loss when the number of femtoseconds is larger than the `float` significand. 140 | This was proposed in an earlier revision of this RFC. 141 | - Rounding to integer femtoseconds is already lossy and a further precision loss from converting to a `float` is negligible in most real world use cases. 142 | For any applications requiring an exact conversion of a `Period`, the `.femtoseconds` property is always exact. 143 | 144 | ## Prior art 145 | [prior-art]: #prior-art 146 | 147 | None. 148 | 149 | ## Unresolved questions 150 | [unresolved-questions]: #unresolved-questions 151 | 152 | None. 153 | 154 | ## Future possibilities 155 | [future-possibilities]: #future-possibilities 156 | 157 | None. 158 | -------------------------------------------------------------------------------- /text/0070-soc-memory-map-names.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-06-28 2 | - RFC PR: [amaranth-lang/rfcs#70](https://github.com/amaranth-lang/rfcs/pull/70) 3 | - Amaranth SoC Issue: [amaranth-lang/amaranth-soc#93](https://github.com/amaranth-lang/amaranth-soc/issues/93) 4 | 5 | # Unify the naming of `MemoryMap` resources and windows 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | - Use a `MemoryMap.Name` class to represent resource and window names. 11 | - Assign window names in `MemoryMap.add_window()` instead of `MemoryMap.__init__()`. 12 | 13 | ## Motivation 14 | [motivation]: #motivation 15 | 16 | In a `MemoryMap`, resources (e.g. registers of a peripheral) and windows (nested memory maps) are added with similar methods: `MemoryMap.add_resource()` and `.add_window()`. 17 | 18 | However, their names are handled differently: 19 | - a resource name is a tuple of strings, assigned as a parameter to `MemoryMap.add_resource()`. 20 | - a window name is a string, assigned at the creation of the window itself (as a parameter to `MemoryMap.__init__()`). 21 | 22 | These differences add needless complexity to the API: 23 | - the name of a window is only relevant from the context of the memory map to which it is added. 24 | - window names may also benefit from being split into tuples, in order to let consumers of the memory map (such as a BSP generator) decide their format. 25 | 26 | Additionally, support for integers in resource and window names is needed to represent indices. A BSP generator may choose to format them in a specific way (e.g. `"foo[1]"`). 27 | 28 | ## Guide-level explanation 29 | [guide-level-explanation]: #guide-level-explanation 30 | 31 | Resource and window names are instances of `MemoryMap.Name`, a subclass of `tuple` which can only contain non-empty strings and non-negative integers. 32 | 33 | Some examples: 34 | 35 | ```python3 36 | >>> MemoryMap.Name(("rx", "status")) 37 | Name('rx', 'status') 38 | >>> MemoryMap.Name(("uart", 0)) 39 | Name('uart', 0) 40 | >>> MemoryMap.Name(MemoryMap.Name(("uart", 0)) 41 | Name('uart', 0) 42 | >>> MemoryMap.Name("foo") 43 | Name('foo',) 44 | ``` 45 | 46 | ### Assigning resource names 47 | 48 | The name of a resource is given by the `name` parameter of `MemoryMap.add_resource()`. For the sake of brevity, users can pass a tuple which is implicitly cast to a `MemoryMap.Name`. 49 | 50 | This example shows how names are assigned to the registers of an UART peripheral: 51 | 52 | ```python3 53 | from amaranth import * 54 | from amaranth.lib import wiring 55 | from amaranth.lib.wiring import In, Out 56 | 57 | from amaranth_soc import csr 58 | from amaranth_soc.memory import MemoryMap 59 | 60 | 61 | class UART(wiring.Component): 62 | class RxConfig(csr.Register, access="rw"): 63 | enable: csr.Field(csr.action.RW, 1) 64 | 65 | class RxStatus(csr.Register, access="rw"): 66 | ready: csr.Field(csr.action.R, 1) 67 | error: csr.Field(csr.action.RW1C, 1) 68 | 69 | class RxData(csr.Register, access="r"): 70 | def __init__(self): 71 | super().__init__(csr.Field(csr.action.R, 8)) 72 | 73 | csr_bus: In(csr.Signature(addr_width=10, data_width=32)) 74 | 75 | def __init__(self): 76 | super().__init__() 77 | 78 | memory_map = MemoryMap(addr_width=10, data_width=32) 79 | memory_map.add_resource(self.RxConfig(), size=1, name=("rx", "config")) 80 | memory_map.add_resource(self.RxStatus(), size=1, name=MemoryMap.Name(("rx", "status"))) 81 | memory_map.add_resource(self.RxData(), size=1, name=("rx", "data")) 82 | memory_map.freeze() 83 | 84 | self.csr_bus.memory_map = memory_map 85 | 86 | def elaborate(self, platform): 87 | m = Module() 88 | # ... 89 | return m 90 | ``` 91 | 92 | ### Assigning window names 93 | 94 | Similarly, the name of a window is given by the `name` parameter of `MemoryMap.add_window()`. Unlike resource names, window names are optional. 95 | 96 | This example shows how names are assigned to two UART peripherals, as their memory maps are added as windows to a bus decoder memory map: 97 | 98 | ```python3 99 | class Decoder(wiring.Component): 100 | csr_bus: In(csr.Signature(addr_width=20, data_width=32)) 101 | 102 | def __init__(self): 103 | super().__init__() 104 | self._subs = dict() 105 | self.csr_bus.memory_map = MemoryMap(addr_width=20, data_width=32) 106 | 107 | def add(self, sub_bus, *, name=None, addr=None): 108 | self._subs[sub_bus.memory_map] = sub_bus 109 | return self.csr_bus.memory_map.add_window(sub_bus.memory_map, name=name, addr=addr) 110 | 111 | def elaborate(self, platform): 112 | m = Module() 113 | 114 | with m.Switch(self.csr_bus.addr): 115 | for window, name, (pattern, ratio) in self.csr_bus.memory_map.window_patterns(): 116 | sub_bus = self._subs[window] 117 | with m.Case(pattern): 118 | pass # ... drive the subordinate bus interface 119 | 120 | return m 121 | 122 | 123 | uart_0 = UART() 124 | uart_1 = UART() 125 | 126 | decoder = Decoder() 127 | decoder.add(uart_0.csr_bus, name=("uart", 0)) 128 | decoder.add(uart_1.csr_bus, name=MemoryMap.Name(("uart", 1))) 129 | ``` 130 | 131 | In a `MemoryMap` hierarchy, each resource is identified by a path. The path of a resource is a tuple ending with its name, preceded by the name of each window that contains it: 132 | 133 | ```python3 134 | >>> for res_info in decoder.csr_bus.memory_map.all_resources(): 135 | ... print(res_info.path) 136 | (Name('uart', 0), Name('rx', 'config')) 137 | (Name('uart', 0), Name('rx', 'status')) 138 | (Name('uart', 0), Name('rx', 'data')) 139 | (Name('uart', 1), Name('rx', 'config')) 140 | (Name('uart', 1), Name('rx', 'status')) 141 | (Name('uart', 1), Name('rx', 'data')) 142 | ``` 143 | 144 | ## Reference-level explanation 145 | [reference-level-explanation]: #reference-level-explanation 146 | 147 | The following changes to the `amaranth_soc.memory` module are made: 148 | 149 | - Add a `MemoryMap.Name(name, /)` class. It is a subclass of `tuple`, where: 150 | 151 | * `name` must either be a string or a tuple of strings and non-negative integers. If `name` is a string, it is implicitly converted to `(name,)`. 152 | 153 | - The following changes to `MemoryMap` are made: 154 | 155 | * The optional `name` parameter of `MemoryMap()` is removed. 156 | * The `MemoryMap.name` property is removed. 157 | * The `name` parameter of `MemoryMap.add_resource()` must be a `MemoryMap.Name`. 158 | * An optional `name` parameter is added to `MemoryMap.add_window()`, which must be a `MemoryMap.Name`. 159 | * The yield values of `MemoryMap.windows()` are changed to `window, name, (start, end, ratio)`. 160 | * The yield values of `MemoryMap.window_patterns()` are changed to `window, name, (pattern, ratio)`. 161 | 162 | - The following changes to `ResourceInfo` are made: 163 | 164 | * The `path` parameter of `ResourceInfo()` must be a tuple of `MemoryMap.Name` objects. 165 | 166 | As a consequence of this proposal, the following changes are made to other modules: 167 | 168 | - `amaranth_soc.csr.bus` and `amaranth_soc.wishbone.bus`: 169 | * The optional `name` parameter of `Decoder()` is moved to `Decoder.add()`. 170 | 171 | - `amaranth_soc.csr.reg`: 172 | * The optional `name` parameter of `Builder()` is removed. 173 | 174 | - `amaranth_soc.csr.wishbone`: 175 | * The optional `name` parameter of `WishboneCSRBridge()` is assigned to `csr_bus.memory_map` (instead of `wb_bus.memory_map`). 176 | 177 | ## Drawbacks 178 | [drawbacks]: #drawbacks 179 | 180 | - This will break codebases that make use of window names. 181 | 182 | ## Rationale and alternatives 183 | [rationale-and-alternatives]: #rationale-and-alternatives 184 | 185 | - Providing a `MemoryMap.Name` class for resource and window names facilitates their validation and documentation. 186 | * ~~Alternative #1: do not add a class, and use standard tuples instead. Names will have to be validated by other means.~~ 187 | * ~~Alternative #2: use `MemoryMap.Name` for resource names only. Window names remain limited to strings.~~ 188 | 189 | ## Prior art 190 | [prior-art]: #prior-art 191 | 192 | - Resource names became tuples of strings as a consequence of [RFC 16](https://amaranth-lang.org/rfcs/0016-soc-csr-regs.html). However, array indices defined with `csr.Builder.Index()` were [cast to strings](https://github.com/amaranth-lang/amaranth-soc/issues/69) when `.as_memory_map()` was called. 193 | 194 | ## Resolved questions 195 | [unresolved-questions]: #unresolved-questions 196 | 197 | - Should we require the presence of at least one string in `MemoryMap.Name` ? 198 | * Empty names are forbidden and transparent windows (i.e. without names) must use `None` instead. Further validation is deferred to consumers of the memory map (e.g. a BSP generator). 199 | - Should we specify the order between strings and integers ? In `csr.Builder`, array indices precede cluster and register names (e.g. `('bar', 0, 'foo')` could be formatted as `"bar.foo[0]"`). 200 | * No, this decision is left out to consumers of the memory map. They may interpret a name differently depending on what it is assigned to. 201 | 202 | ## Future possibilities 203 | [future-possibilities]: #future-possibilities 204 | 205 | None. 206 | -------------------------------------------------------------------------------- /text/0071-enumview-matches.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-07-29 2 | - RFC PR: [amaranth-lang/rfcs#71](https://github.com/amaranth-lang/rfcs/pull/71) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1468](https://github.com/amaranth-lang/amaranth/issues/1468) 4 | 5 | # `EnumView.matches` 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | A `matches` method is added to `EnumView`, performing a similar function to `Value.matches`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Amaranth currently has `Value.matches`, which checks whether a value matches any of the given patterns. An obvious application of such a feature is using it with an enumerated value. However, as it stands, `EnumView` doesn't provide a corresponding method natively, requiring a cast to `Value`. Additionally, casting the `EnumView` to `Value` erases the enumeration type, providing no protection against matching with values from the wrong type. 16 | 17 | This RFC proposes to fill that gap by adding a type-safe `EnumView.matches` method. 18 | 19 | ## Guide-level explanation 20 | [guide-level-explanation]: #guide-level-explanation 21 | 22 | The `EnumView.matches` method can be used to check whether an enumerated value belongs to a given set: 23 | 24 | ``` 25 | class Color(enum.Enum, shape=3): 26 | BLACK = 0 27 | RED = 1 28 | GREEN = 2 29 | BLUE = 4 30 | WHITE = 7 31 | 32 | s = Signal(Color) 33 | m.d.comb += is_grayscale.eq(s.matches(Color.BLACK, Color.WHITE)) 34 | ``` 35 | 36 | ## Reference-level explanation 37 | [reference-level-explanation]: #reference-level-explanation 38 | 39 | The following method is added: 40 | 41 | - `EnumView.matches(*values)`: equivalent to `self.as_value().matches(*values)`, but requires all `values` to be a member of the same enum as the `EnumView`, raising a `TypeError` otherwise 42 | 43 | ## Drawbacks 44 | [drawbacks]: #drawbacks 45 | 46 | - more moving parts in the language 47 | - more general pattern matching facilities have been requested on multiple occasions, which this facility could end up redundant with 48 | 49 | ## Rationale and alternatives 50 | [rationale-and-alternatives]: #rationale-and-alternatives 51 | 52 | The proposed functionality is an obvious extension of `Value.matches` to the `EnumView` class. The proposed typechecking is consistent with current `EnumView` behavior. 53 | 54 | ## Prior art 55 | [prior-art]: #prior-art 56 | 57 | `Value.matches`, Rust `matches!()`. 58 | 59 | ## Unresolved questions 60 | [unresolved-questions]: #unresolved-questions 61 | 62 | None. 63 | 64 | ## Future possibilities 65 | [future-possibilities]: #future-possibilities 66 | 67 | None for `EnumView`. However, there have been discussions of having more advanced pattern matching facilities in the language in general. -------------------------------------------------------------------------------- /text/0073-stricter-connections.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2024-09-16 2 | - RFC PR: [amaranth-lang/rfcs#73](https://github.com/amaranth-lang/rfcs/pull/73) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1511](https://github.com/amaranth-lang/amaranth/issues/1511) 4 | 5 | # Stricter connections 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Make it possible for `lib.wiring.connect()` to reject connections between ports with shapes that despite having equal widths are incompatible. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | The primary motivating use case is to be able to prevent connecting stream interfaces with incompatible payload shapes. 16 | 17 | ## Guide-level explanation 18 | [guide-level-explanation]: #guide-level-explanation 19 | 20 | `lib.stream` defines a basic stream interface with `ready`, `valid` and `payload` members. 21 | Additional metadata signals can be added by making the `payload` shape an aggregate. 22 | This has the advantage that such streams can be passed through standard components like FIFOs without them having to know or care about these signals. 23 | 24 | Consider how we can make a byte-wide packetized stream by defining the `payload` shape as `StructLayout({"data": 8, "last": 1})`. 25 | The `data` field contains the data byte and the `last` field is a flag indicating that the current byte is the last in the packet. 26 | 27 | This is not the only common way to make a packetized stream. 28 | Instead of (or in addition to) the `last` flag, the payload could have a `first` flag, indicating that the current byte is the first in the packet. 29 | Both are valid options with different semantics for different usecases. 30 | 31 | The problem is that `lib.wiring.connect()` currently only checks that port members have matching widths, so a connection between a stream interface with `last` semantics and a stream interface with `first` semantics will not be rejected, despite being incompatible. 32 | 33 | Currently, `lib.data.View.eq()` does no checks on the passed value and immediately forwards to `self.as_value().eq()`, making this legal: 34 | 35 | ```python 36 | >>> a = Signal(StructLayout({"data": 8, "last": 1})) 37 | >>> b = Signal(StructLayout({"data": 8, "first": 1})) 38 | >>> a.eq(b) 39 | (eq (sig a) (sig b)) 40 | ``` 41 | 42 | This RFC proposes adding a check to `View.eq()` that would reject direct assignments from another aggregate data structure with a non-identical layout. 43 | If such an assignment is desired, the other aggregate data structure can be explicitly passed through `Value.cast()` first. 44 | 45 | Currently `lib.wiring.connect()` passes every signal through `Value.cast()` before assigning them. 46 | This results in a `ValueCastable`'s `.eq()` not being called, and thereby bypassing the check proposed above. 47 | 48 | This RFC proposes removing the `Value.cast()` so a `ValueCastable`'s `.eq()` will be called directly. 49 | 50 | This RFC proposes updating `lib.enum.EnumView` in the same manner, for the same reason, as `lib.data.View`. 51 | 52 | ## Reference-level explanation 53 | [reference-level-explanation]: #reference-level-explanation 54 | 55 | Modify `lib.wiring.connect()` to not pass port members through `Value.cast()`, so that a `ValueCastable`'s `.eq()` will be called, allowing it to perform compatibility checks. 56 | - If a `ValueCastable` doesn't define `.eq()`, reject the assignment. 57 | 58 | Modify `lib.data.View.eq(other)` to add the following checks: 59 | - If `other` is a `ValueCastable`, do `Layout.cast(other.shape())` 60 | - If a valid `Layout` is returned, reject the assignment if it doesn't match `self.shape()`. 61 | - If `Layout.cast()` raises, reject the assignment. 62 | - Otherwise, proceed as normal. 63 | 64 | Modify `lib.enum.EnumView.eq(other)` to add the following checks: 65 | - If `other` is a `ValueCastable`, reject the assignment if `other.shape()` doesn't match `self.shape()`. 66 | - Otherwise, proceed as normal. 67 | 68 | Rejected assignments are a warning in Amaranth 0.6 and becomes a hard error in Amaranth 0.7. 69 | 70 | ## Drawbacks 71 | [drawbacks]: #drawbacks 72 | 73 | - Increased language complexity. 74 | 75 | - This will add an implied requirement for a `ValueCastable` to implement `.eq()` to be usable with `lib.wiring`. Currently a `ValueCastable` is not required to implement `.eq()` at all. 76 | - We could fall back to `Value.cast().eq()` when `.eq()` is not defined. 77 | 78 | ## Rationale and alternatives 79 | [rationale-and-alternatives]: #rationale-and-alternatives 80 | 81 | - Signals like `first` and `last` could be added as separate ports in the stream signature, preventing incompatible connections. 82 | - This was already discussed in [RFC 61](0061-minimal-streams.md) and decided against. 83 | - An explicit hook for checking compatibility between interfaces could be added, instead of relying on `.eq()`. 84 | - This RFC proposes picking the low-hanging fruits first, making use of already existing mechanisms. 85 | If this is not enough, another RFC can propose an explicit hook later. 86 | 87 | ## Prior art 88 | [prior-art]: #prior-art 89 | 90 | [RFC 61](0061-minimal-streams.md) already discussed the need to eventually make `lib.wiring.connect()` stricter to prevent the connection of stream interfaces with incompatible payloads. 91 | 92 | ## Unresolved questions 93 | [unresolved-questions]: #unresolved-questions 94 | 95 | None. 96 | 97 | ## Future possibilities 98 | [future-possibilities]: #future-possibilities 99 | 100 | - A hook can still be added to allow a signature/interface to perform an explicit compatibility check, for cases where signatures have identical members but still have metadata indicating they are incompatible. 101 | - This hook could also allow overriding the existing checks, allowing connections where interfaces are compatible despite differences in members. 102 | -------------------------------------------------------------------------------- /text/0074-structured-vcd/gtkwave_post_rfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amaranth-lang/rfcs/a838a3b4a06db6f6a216de1980dd9d24a148d56a/text/0074-structured-vcd/gtkwave_post_rfc.png -------------------------------------------------------------------------------- /text/0074-structured-vcd/gtkwave_pre_rfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amaranth-lang/rfcs/a838a3b4a06db6f6a216de1980dd9d24a148d56a/text/0074-structured-vcd/gtkwave_pre_rfc.png -------------------------------------------------------------------------------- /text/0074-structured-vcd/surfer_post_rfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amaranth-lang/rfcs/a838a3b4a06db6f6a216de1980dd9d24a148d56a/text/0074-structured-vcd/surfer_post_rfc.png -------------------------------------------------------------------------------- /text/0074-structured-vcd/surfer_pre_rfc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amaranth-lang/rfcs/a838a3b4a06db6f6a216de1980dd9d24a148d56a/text/0074-structured-vcd/surfer_pre_rfc.png -------------------------------------------------------------------------------- /text/0076-amaranth-boards-versioning-policy.md: -------------------------------------------------------------------------------- 1 | - Start Date: (fill in with date at which the RFC is merged, 2025-05-12) 2 | - RFC PR: [amaranth-lang/rfcs#76](https://github.com/amaranth-lang/rfcs/pull/76) 3 | - Amaranth Issue: [amaranth-lang/amaranth#1595](https://github.com/amaranth-lang/amaranth/issues/1595) 4 | 5 | # `amaranth-boards` versioning policy 6 | 7 | ## Summary 8 | [summary]: #summary 9 | 10 | Add a versioning policy for `amaranth-boards`. 11 | 12 | ## Motivation 13 | [motivation]: #motivation 14 | 15 | Packages with direct (e.g. git) dependencies aren't allowed on PyPI, so by not publishing releases on PyPI, we're blocking downstream projects depending on `amaranth-boards` from publishing their releases on PyPi without having to do workarounds. 16 | 17 | ## Explanation 18 | [guide-level-explanation]: #guide-level-explanation 19 | 20 | - Use a `0.x.y` versioning scheme. 21 | - Increment `y` and do a new release any time there's been done significant additions. 22 | - Increment `x`, reset `y` and do a new release any time there are breaking changes. 23 | 24 | A breaking change is anything that requires manual intervention for code that previously worked fine. 25 | I.e. a change to a board file which previously had two pins incorrectly swapped would not be a breaking change; a change to names of resources across multiple boards would be. 26 | 27 | This should be implemented by automatically releasing every commit to `main` whose HEAD message doesn't contain `[breaking change]` or `[breaking-change]` with a version like `v0.x.y` where `y` is the number of commits since the last breaking change, and automatically tagging every commit that does with a tag `v0.(x+1).0`. 28 | 29 | ## Drawbacks 30 | [drawbacks]: #drawbacks 31 | 32 | None. 33 | 34 | ## Rationale and alternatives 35 | [rationale-and-alternatives]: #rationale-and-alternatives 36 | 37 | `amaranth-boards` is mainly a collection of independent board definitions where most changes consists of adding or updating definitions for one specific target. 38 | 39 | Most changes will have none to limited impact on existing code, and a simple policy reducing friction for new additions and fixes is more useful than having a strict deprecation policy with regular major releases. 40 | 41 | ## Prior art 42 | [prior-art]: #prior-art 43 | 44 | None. 45 | 46 | ## Unresolved questions 47 | [unresolved-questions]: #unresolved-questions 48 | 49 | None. 50 | 51 | ## Future possibilities 52 | [future-possibilities]: #future-possibilities 53 | 54 | None. 55 | -------------------------------------------------------------------------------- /theme/img-color-scheme.css: -------------------------------------------------------------------------------- 1 | .light, .rust { 2 | img { 3 | color-scheme: light; 4 | } 5 | } 6 | .ayu, .coal, .navy { 7 | img { 8 | color-scheme: dark; 9 | } 10 | } 11 | --------------------------------------------------------------------------------