diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index c41a0cacb..f56769001 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -16,6 +16,8 @@ jobs:
- '3.10'
- '3.11'
- '3.12'
+ - '3.13'
+ - '3.14'
- 'pypy-3.8'
- 'pypy-3.9'
- 'pypy-3.10'
@@ -23,7 +25,7 @@ jobs:
allow-failure:
- false
include:
- - python-version: '3.13-dev'
+ - python-version: '3.15-dev'
allow-failure: true
continue-on-error: ${{ matrix.allow-failure }}
name: 'test (${{ matrix.python-version }})'
diff --git a/amaranth/cli.py b/amaranth/cli.py
index 87ff10e2f..0fd351ec8 100644
--- a/amaranth/cli.py
+++ b/amaranth/cli.py
@@ -22,16 +22,16 @@ def main_parser(parser=None):
p_generate.add_argument("--no-src", dest="emit_src", default=True, action="store_false",
help="suppress generation of source location attributes")
p_generate.add_argument("generate_file",
- metavar="FILE", type=argparse.FileType("w"), nargs="?",
+ metavar="FILE", type=str, nargs="?",
help="write generated code to FILE")
p_simulate = p_action.add_parser(
"simulate", help="simulate the design")
p_simulate.add_argument("-v", "--vcd-file",
- metavar="VCD-FILE", type=argparse.FileType("w"),
+ metavar="VCD-FILE", type=str,
help="write execution trace to VCD-FILE")
p_simulate.add_argument("-w", "--gtkw-file",
- metavar="GTKW-FILE", type=argparse.FileType("w"),
+ metavar="GTKW-FILE", type=str,
help="write GTKWave configuration to GTKW-FILE")
p_simulate.add_argument("-p", "--period", dest="sync_period",
metavar="TIME", type=float, default=1e-6,
@@ -48,11 +48,11 @@ def main_runner(parser, args, design, platform=None, name="top", ports=()):
fragment = Fragment.get(design, platform)
generate_type = args.generate_type
if generate_type is None and args.generate_file:
- if args.generate_file.name.endswith(".il"):
+ if args.generate_file.endswith(".il"):
generate_type = "il"
- if args.generate_file.name.endswith(".cc"):
+ if args.generate_file.endswith(".cc"):
generate_type = "cc"
- if args.generate_file.name.endswith(".v"):
+ if args.generate_file.endswith(".v"):
generate_type = "v"
if generate_type is None:
parser.error("Unable to auto-detect language, specify explicitly with -t/--type")
@@ -63,7 +63,8 @@ def main_runner(parser, args, design, platform=None, name="top", ports=()):
if generate_type == "v":
output = verilog.convert(fragment, name=name, ports=ports, emit_src=args.emit_src)
if args.generate_file:
- args.generate_file.write(output)
+ with open(args.generate_file, "w") as f:
+ f.write(output)
else:
print(output)
diff --git a/amaranth/lib/data.py b/amaranth/lib/data.py
index 87624f0a4..d49f1c9cc 100644
--- a/amaranth/lib/data.py
+++ b/amaranth/lib/data.py
@@ -3,6 +3,10 @@
from collections.abc import Mapping, Sequence
import warnings
import operator
+try:
+ import annotationlib # py3.14+
+except ImportError:
+ annotationlib = None # py3.13-
from amaranth._utils import final
from amaranth.hdl import *
@@ -66,6 +70,9 @@ def width(self):
"""
return Shape.cast(self.shape).width
+ def __hash__(self):
+ return hash((self.shape, self.offset))
+
def __eq__(self, other):
"""Compare fields.
@@ -167,6 +174,11 @@ def as_shape(self):
"""
return unsigned(self.size)
+ def __hash__(self):
+ if not hasattr(self, "_Layout__hash"):
+ setattr(self, "_Layout__hash", hash((self.size, frozenset(iter(self)))))
+ return self.__hash
+
def __eq__(self, other):
"""Compare layouts.
@@ -1124,6 +1136,9 @@ def __len__(self):
f"`len()` can only be used on constants of array layout, not {self.__layout!r}")
return self.__layout.length
+ def __hash__(self):
+ return hash((self.__target, self.__layout))
+
def __eq__(self, other):
if isinstance(other, View) and self.__layout == other._View__layout:
return self.as_value() == other._View__target
@@ -1198,7 +1213,19 @@ def __repr__(self):
class _AggregateMeta(ShapeCastable, type):
def __new__(metacls, name, bases, namespace):
- if "__annotations__" not in namespace:
+ annotations = None
+ skipped_annotations = set()
+ wrapped_annotate = None
+ if annotationlib is not None:
+ if annotate := annotationlib.get_annotate_from_class_namespace(namespace):
+ annotations = annotationlib.call_annotate_function(
+ annotate, format=annotationlib.Format.VALUE)
+ def wrapped_annotate(format):
+ annos = annotationlib.call_annotate_function(annotate, format, owner=cls)
+ return {k: v for k, v in annos.items() if k not in skipped_annotations}
+ else:
+ annotations = namespace.get("__annotations__")
+ if annotations is None:
# This is a base class without its own layout. It is not shape-castable, and cannot
# be instantiated. It can be used to share behavior.
return type.__new__(metacls, name, bases, namespace)
@@ -1207,13 +1234,14 @@ def __new__(metacls, name, bases, namespace):
# be instantiated. It can also be subclassed, and used to share layout and behavior.
layout = dict()
default = dict()
- for field_name in {**namespace["__annotations__"]}:
+ for field_name in {**annotations}:
try:
- Shape.cast(namespace["__annotations__"][field_name])
+ Shape.cast(annotations[field_name])
except TypeError:
# Not a shape-castable annotation; leave as-is.
continue
- layout[field_name] = namespace["__annotations__"].pop(field_name)
+ skipped_annotations.add(field_name)
+ layout[field_name] = annotations.pop(field_name)
if field_name in namespace:
default[field_name] = namespace.pop(field_name)
cls = type.__new__(metacls, name, bases, namespace)
@@ -1224,6 +1252,8 @@ def __new__(metacls, name, bases, namespace):
.format(", ".join(default.keys())))
cls.__layout = cls.__layout_cls(layout)
cls.__default = default
+ if wrapped_annotate is not None:
+ cls.__annotate__ = wrapped_annotate
return cls
else:
# This is a class that has a base class with a layout and annotations. Such a class
diff --git a/amaranth/lib/wiring.py b/amaranth/lib/wiring.py
index b2a758515..8b8fc1c40 100644
--- a/amaranth/lib/wiring.py
+++ b/amaranth/lib/wiring.py
@@ -2,6 +2,10 @@
import enum
import re
import warnings
+try:
+ import annotationlib # py3.14+
+except ImportError:
+ annotationlib = None # py3.13-
from .. import tracer
from ..hdl._ast import Shape, ShapeCastable, Const, Signal, Value, ValueCastable
@@ -1669,7 +1673,14 @@ def __init__(self, signature=None, *, src_loc_at=0):
cls = type(self)
members = {}
for base in reversed(cls.mro()[:cls.mro().index(Component)]):
- for name, annot in base.__dict__.get("__annotations__", {}).items():
+ annotations = None
+ if annotationlib is not None:
+ if annotate := annotationlib.get_annotate_from_class_namespace(base.__dict__):
+ annotations = annotationlib.call_annotate_function(
+ annotate, format=annotationlib.Format.VALUE)
+ if annotations is None:
+ annotations = base.__dict__.get("__annotations__", {})
+ for name, annot in annotations.items():
if name.startswith("_"):
continue
if type(annot) is Member:
diff --git a/amaranth/tracer.py b/amaranth/tracer.py
index 4c059d0f1..36b6ae745 100644
--- a/amaranth/tracer.py
+++ b/amaranth/tracer.py
@@ -58,8 +58,8 @@ def get_var_name(depth=2, default=_raise_exception):
return code.co_cellvars[imm]
else:
return code.co_freevars[imm - len(code.co_cellvars)]
- elif opc in ("LOAD_GLOBAL", "LOAD_NAME", "LOAD_ATTR", "LOAD_FAST", "LOAD_DEREF",
- "DUP_TOP", "BUILD_LIST", "CACHE", "COPY"):
+ elif opc in ("LOAD_GLOBAL", "LOAD_NAME", "LOAD_ATTR", "LOAD_FAST", "LOAD_FAST_BORROW",
+ "LOAD_DEREF", "DUP_TOP", "BUILD_LIST", "CACHE", "COPY"):
imm = 0
index += 2
else:
diff --git a/amaranth/vendor/_gowin.py b/amaranth/vendor/_gowin.py
index fa063c034..95adc82fe 100644
--- a/amaranth/vendor/_gowin.py
+++ b/amaranth/vendor/_gowin.py
@@ -442,7 +442,7 @@ def _osc_div(self):
"{{name}}.sdc": r"""
// {{autogenerated}}
{% for signal, frequency in platform.iter_signal_clock_constraints() -%}
- create_clock -name {{ "{" }}{{signal.name}}{{ "}" }} -period {{1000000000/frequency}} [get_nets {{signal|hierarchy("/")|tcl_quote}}]
+ create_clock -name {{ "{" }}{{signal.name}}{{ "}" }} -period {{1000000000/frequency}} [get_nets {{ "{" }}{{signal|hierarchy("/")}}{{ "}" }}]
{% endfor %}
{% for port, frequency in platform.iter_port_clock_constraints() -%}
create_clock -name {{ "{" }}{{port.name}}{{ "}" }} -period {{1000000000/frequency}} [get_ports
diff --git a/docs/changes.rst b/docs/changes.rst
index 9a9ad6c34..ff58e6124 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -9,6 +9,7 @@ Documentation for past releases
Documentation for past releases of the Amaranth language and toolchain is available online:
+* `Amaranth 0.5.7 `_
* `Amaranth 0.5.6 `_
* `Amaranth 0.5.5 `_
* `Amaranth 0.5.4 `_
@@ -25,6 +26,12 @@ Documentation for past releases of the Amaranth language and toolchain is availa
* `Amaranth 0.3 `_
+Version 0.5.8
+=============
+
+Updated to address compatibility with Python 3.14.
+
+
Version 0.5.7
=============
diff --git a/tests/test_lib_data.py b/tests/test_lib_data.py
index bf2583eeb..28aee6d97 100644
--- a/tests/test_lib_data.py
+++ b/tests/test_lib_data.py
@@ -1,6 +1,10 @@
from enum import Enum
import operator
from unittest import TestCase
+try:
+ import annotationlib # py3.14+
+except ImportError:
+ annotationlib = None # py3.13-
from amaranth.hdl import *
from amaranth.lib import data
@@ -74,6 +78,9 @@ def test_immutable(self):
with self.assertRaises(AttributeError):
data.Field(1, 0).offset = 1
+ def test_hash(self):
+ hash(data.Field(unsigned(2), 1))
+
class StructLayoutTestCase(FHDLTestCase):
def test_construct(self):
@@ -520,6 +527,9 @@ def test_signal_init(self):
self.assertEqual(Signal(sl).as_value().init, 0)
self.assertEqual(Signal(sl, init={"a": 0b1, "b": 0b10}).as_value().init, 5)
+ def test_hash(self):
+ hash(data.StructLayout({}))
+
class ViewTestCase(FHDLTestCase):
def test_construct(self):
@@ -1167,6 +1177,9 @@ def test_repr(self):
s1 = data.Const(data.StructLayout({"a": unsigned(2)}), 2)
self.assertRepr(s1, "Const(StructLayout({'a': unsigned(2)}), 2)")
+ def test_hash(self):
+ hash(data.Const(data.StructLayout({"a": unsigned(2)}), 2))
+
class StructTestCase(FHDLTestCase):
def test_construct(self):
@@ -1310,7 +1323,11 @@ class S(data.Struct):
c: str = "x"
self.assertEqual(data.Layout.cast(S), data.StructLayout({"a": unsigned(1)}))
- self.assertEqual(S.__annotations__, {"b": int, "c": str})
+ if annotationlib is not None:
+ annotations = annotationlib.get_annotations(S, format=annotationlib.Format.VALUE)
+ else:
+ annotations = S.__annotations__
+ self.assertEqual(annotations, {"b": int, "c": str})
self.assertEqual(S.c, "x")
def test_signal_like(self):