Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# Copyright 2025 LibreLane Contributors
#
# Adapted from OpenLane 2
#
# Copyright 2023 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import shlex
import inspect
from enum import Enum
from decimal import Decimal, InvalidOperation
from dataclasses import (
MISSING,
asdict,
dataclass,
field,
fields,
is_dataclass,
)
import types
import textwrap
from typing import (
ClassVar,
Dict,
Iterable,
List,
Literal,
Optional,
Set,
Tuple,
Union,
Mapping,
Callable,
Type,
Any,
get_origin,
get_args,
)
from ..state import DesignFormat, State
from ..common import GenericDict, Path, is_string, zip_first, Number, slugify
# Scalar = Union[Type[str], Type[Decimal], Type[Path], Type[bool]]
# VType = Union[Scalar, List[Scalar]]
class MissingRequiredVariable(ValueError):
def __init__(self, variable: "Variable", pdk: bool = False) -> None:
self.variable = variable
if self.variable.pdk:
super().__init__(
f"Required PDK variable '{self.variable.name}' did not get a specified value. This PDK may be incompatible with your flow."
)
else:
super().__init__(
f"Required variable '{self.variable.name}' did not get a specified value."
)
class Orientation(str, Enum):
N = "N"
FN = "FN"
W = "W"
FW = "FW"
S = "S"
FS = "FS"
E = "E"
FE = "FE"
# OpenAccess
R0 = "N"
MY = "FN"
R90 = "W"
MXR90 = "FW"
R180 = "S"
MX = "FS"
R270 = "E"
MYR90 = "FE"
def __str__(self) -> str:
return self.value
@dataclass
class Instance:
"""
Location information for an instance of a cell or macro.
:param location: The physical co-ordinates of the object's origin. Leave
empty for automatic placement.
:param orientation: The orientation of the object's placement. 'N'/'R0' by default.
"""
location: Optional[Tuple[Decimal, Decimal]]
orientation: Optional[Orientation]
@dataclass
class Macro:
"""
A data structure for storing definitions of Macros.
As it is typically stored in a dictionary in its superclass, the module name
is not stored in any of these fields.
You will note most fields correspond to a :class:`librelane.state.DesignFormat`
entry IDs. This is not coincidental.
:param gds: A list of GDSII files representing the design. At least one is required.
:param lef: A list of LEF files representing the design. At least one is required.
:param instances: A dictionary of :class:`Instance` objects representing the
instances of said macro.
The keys for the dictionaries are the names of the instances.
:param nl: A list of netlists constituting the design.
The netlists must be valid Verilog netlists readable by tools such as
OpenSTA.
Can be empty, however SPEF-based hierarchical static timing analysis
will be unavailable.
:param spef: A dictionary of parasitics annotations for the various netlists of the
Macro.
The keys are wildcards for timing corners supported by a certain SPEF file.
Can be empty, however SPEF-based hierarchical static timing analysis
will be unavailable.
:param lib: A dictionary of timing library files.
The keys are wildcards for timing corners supported by a certain LIB file.
If both SPEF and LIB views are empty, the design may be black-boxed
during STA.
:param spice: A list of SPICE netlists constituting the design. May be
useful in some flows.
:param sdf: A dictionary of standard delay format files. May be useful in some flows.
The keys are wildcards for timing corners supported by a certain SPEF file.
:param json_h: A JSON file as generated by Yosys. Helpful in some flows.
"""
gds: List[Path]
lef: List[Path]
instances: Dict[str, Instance] = field(default_factory=lambda: {})
vh: List[Path] = field(default_factory=lambda: [])
nl: List[Path] = field(default_factory=lambda: [])
pnl: List[Path] = field(default_factory=lambda: [])
spef: Dict[str, List[Path]] = field(default_factory=lambda: {})
lib: Dict[str, List[Path]] = field(default_factory=lambda: {})
spice: List[Path] = field(default_factory=lambda: [])
sdf: Dict[str, List[Path]] = field(default_factory=lambda: {})
json_h: Optional[Path] = None
def view_by_df(
self, df: DesignFormat
) -> Union[None, Path, List[Path], Dict[str, List[Path]]]:
try:
return getattr(self, df.id)
except AttributeError:
return None
def __post_init__(self):
if len(self.gds) < 1:
raise ValueError(
"Macro definition invalid- at least one GDSII file must be specified."
)
if len(self.lef) < 1:
raise ValueError(
"Macro definition invalid- at least one LEF file must be specified."
)
def __repr__(self) -> str:
return f"{self.__class__.__qualname__}(%s)" % ", ".join(
[f"{k}={repr(v)}" for k, v in asdict(self).items()]
)
def __str__(self) -> str:
return self.__repr__()
@classmethod
def from_state(Self, state: State) -> "Macro":
kwargs = {}
for macro_field in fields(Self):
views = state.get(macro_field.name)
if views is None:
if macro_field.default_factory is not MISSING:
kwargs[macro_field.name] = macro_field.default_factory()
elif macro_field.default is not MISSING:
kwargs[macro_field.name] = macro_field.default
else: # gds or lef
raise ValueError(
f"Macro cannot be made out of input state: View {macro_field.name} is missing"
)
continue
var_name = f"{Self.__name__}.{macro_field.name}"
_, final = Variable(var_name, macro_field.type, "").compile(
GenericDict({var_name: views}),
warning_list_ref=[],
permissive_typing=True,
)
kwargs[macro_field.name] = final
return Self(**kwargs) # type: ignore
def instantiate(
self,
instance_name: str,
location: Tuple[Number, Number],
orientation: Orientation = Orientation.N,
):
location = (Decimal(location[0]), Decimal(location[1]))
self.instances[instance_name] = Instance(location, Orientation[orientation])
def is_optional(t: Type[Any]) -> bool:
type_args = get_args(t)
origin = get_origin(t)
return (origin is Union or origin is types.UnionType) and type(None) in type_args
def some_of(t: Type[Any]) -> Type[Any]:
if not is_optional(t):
return t
# t must be a Union with None if we're here
type_args = get_args(t)
origin = get_origin(t)
args_without_none = [arg for arg in type_args if arg is not type(None)]
if len(args_without_none) == 1:
return args_without_none[0]
if origin is types.UnionType:
# Use the | operator to create a UnionType
result = args_without_none[0]
for arg in args_without_none[1:]:
result = result | arg
return result
# Otherwise, return a typing.Union
new_union = Union[tuple(args_without_none)] # type: ignore
return new_union # type: ignore
def repr_type(t: Type[Any], for_document: bool = False) -> str: # pragma: no cover
optional = is_optional(t)
some = some_of(t)
if hasattr(some, "__name__"): # Python 3.10+
type_string = some.__name__
else:
type_string = str(some)
if is_dataclass(t):
type_string = (
f"{{class}}`{some.__qualname__} <{some.__module__}.{some.__qualname__}>`"
)
separator = "|<br />" if for_document else "|"
if inspect.isclass(some) and issubclass(some, Enum):
type_string = separator.join([str(e.name) for e in some])
type_string = f"`{type_string}`"
else:
origin, args = get_origin(some), get_args(some)
if origin is not None:
if origin == Union:
arg_strings = [repr_type(arg) for arg in args]
type_string = separator.join(arg_strings)
type_string = f"({type_string})"
elif origin == Literal:
return separator.join([repr(arg) for arg in args])
else:
arg_strings = [repr_type(arg) for arg in args]
type_string = f"{type_string}[{', '.join(arg_strings)}]"
return type_string + ("?" if optional else "")
@dataclass
class Variable:
"""
An object encapsulating metadata on an LibreLane configuration variable, which
is used to name, document and validate values supplied to
:class:`librelane.steps.Step`\\s or :class:`librelane.flows.Flow`\\s.
Values supplied for configuration variables are the primary interface by
which users configure LibreLane flows.
:param name: A string name for the Variable. Because of backwards
compatibility with OpenLane, the convention is ``UPPER_SNAKE_CASE``.
:param type: A Python type object representing the variable.
Supported scalars:
- ``int``
- ``decimal.Decimal``
- ``bool``
- ``str``
- :class:`Path`
Supported products:
- ``Union`` (incl. ``Optional``)
- ``List``
- ``Tuple``
- ``Dict``
- ``Enum``
Other:
- ``dataclass`` types composed of the above.
- Sum classes of the above.
:param description: A human-readable description of the variable. Used to
generate help strings and documentation.
:param default: A default value for the variable.
Optional variables have an implicit default value of ``None``.
:param deprecated_names: A list of deprecated names for said variable.
An element of the list can alternative be a tuple of a name and a
Callable used to perform a translation for when a renamed variable is
also slightly modified.
:param units: Used only in documentation: the unit corresponding to this
object, i.e., µm, pF, etc. Can be any string, but for consistency, SI
units must be represented in terms of their official symbols.
:param pdk: Whether this variable is expected to be given a default value
by a PDK or not.
If this is true, and the variable is not of an option type, a PDK *must*
give this variable a default value in order to be marked compatible
with a step.
If this is true and the variable is of an option type, a PDK may
optionally provide a default value for this variable, however steps
must presume it is ``null``.
If this is false, a PDK is not allowed to set a default value for
this variable. In current versions of LibreLane, the value will be
silently ignored, but warnings or errors may occur in future versions.
:param validator: A customizable validator that is run AFTER type checks
and conversions. Takes three arguments:
* variable - the Variable object itself
* input - the user's input, whatever type it may be
* warning_list_ref - a list that may be appended to to emit warnings
The validator may update the value, however it is discouraged to do so
silently (i.e. it's best to emit a warning.) A validator may reject
user input by raising a ``ValueError``.
"""
known_variable_names: ClassVar[Set[str]] = set()
name: str
type: Any
description: str
default: Any = None
deprecated_names: List[Union[str, Tuple[str, Callable]]] = field(
default_factory=list
)
units: Optional[str] = None
pdk: bool = False
validator: Callable[["Variable", Any, List[str]], Any] = lambda self, i, _: i
def __post_init__(self):
Variable.known_variable_names.add(self.name)
for name in self.deprecated_names:
if isinstance(name, tuple):
name, _ = name
Variable.known_variable_names.add(name)
@property
def optional(self) -> bool:
"""
:returns: Whether a variable's type is an `Option type <https://en.wikipedia.org/wiki/Option_type>`_.
"""
return is_optional(self.type)
@property
def some(self) -> Any:
"""
:returns: The type of a variable presuming it is not None.
If a variable is not Optional, that is simply the type specified in the
``type`` attribute.
"""
return some_of(self.type)
def type_repr_md(self, for_document: bool = False) -> str: # pragma: no cover
"""
:param for_document: Adds HTML line breaks between sum type separators
for easier wrapping by web browsers/PDF renderers/what have you
:returns: A pretty Markdown string representation of the Variable's type.
"""
return repr_type(self.type, for_document=for_document)
def desc_repr_md(self) -> str: # pragma: no cover
"""
:returns: The description, but with newlines escaped for Markdown.
"""
return self.description.replace("\n", "<br />")
@staticmethod
def _render_table_md(
vars: Iterable["Variable"],
*,
myst_anchor_owner_id: Optional[str] = None,
) -> str: # pragma; no cover
"""
Renders a markdown table for any iterable of configuration variables.
:param vars: Any iterable object returning configuration variables
:param myst_anchor_owner_id:
If set, the table is rendered for MyST using a mix of HTML and
Markdown, with anchors and a detail tag containing deprecated names.
For universal flow variables, set the anchor id to "".
:returns: A markdown string representing the table
"""
include_units = any(c.units is not None for c in vars)
if myst_anchor_owner_id is None:
# Markdown mode
result = textwrap.dedent(
f"""
| Variable Name | Type | Description | Default | {'Units |' * include_units}
| - | - | - | - | {'- |' * include_units}
"""
)
for var in vars:
units = var.units or ""
pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
result += f"| `{var.name}` {pdk_superscript} | {var.type_repr_md(for_document=True)} | {var.desc_repr_md()} | `{var.default}` |"
if include_units:
result += f" {units} |"
result += "\n"
result += "\n"
else:
if myst_anchor_owner_id == "":
# for _get_docs_identifier, where None is the behavior we want
# for a literal ""
myst_anchor_owner_id = None
result = textwrap.dedent(
f"""
<div class="table-wrapper colwidths-auto docutils container">
<table class="docutils align-default">
<thead><tr>
<th class="head">Variable Name</th>
<th class="head">Type</th>
<th class="head">Description</th>
<th class="head">Default</th>
{'<th class="head">Units</th>' * include_units}
</tr></thead>
<tbody>
"""
)
for var in vars:
units = var.units or ""
pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
var_anchor = f"{{#{var._get_docs_identifier(myst_anchor_owner_id)}}}"
result += textwrap.dedent(
f"""
<tr>
<td>
`{var.name}`{var_anchor} {pdk_superscript}
"""
)
if len(var.deprecated_names):
result += "<details><summary>Deprecated names</summary>\n\n"
for dn in var.deprecated_names:
if isinstance(dn, tuple):
dn = dn[0]
result += f"* `{dn}`\n"
result += "\n</details>\n"
result += textwrap.dedent(
f"""
</td>
<td>
{var.type_repr_md(for_document=True)}
</td>
<td>
{var.desc_repr_md()}
</td>
<td>
`{var.default}`
</td>
"""
)
result += include_units * textwrap.dedent(
f"""
<td>
{units}
</td>
"""
)
result += "\n</tr>"
result += "</tbody></table></div>\n"
return result
def __process(
self,
key_path: str,
value: Any,
validating_type: Type[Any],
default: Any = None,
explicitly_specified: bool = True,
permissive_typing: bool = False,
depth: int = 0,
):
if value is None:
if explicitly_specified:
# User explicitly specified "null" for this value: only error if
# value is not optional
if not is_optional(validating_type):
raise ValueError(
f"Non-optional variable '{key_path}' explicitly assigned a null value."
)
else:
return None
else:
# User did not specify a value for this variable: couple outcomes
if default is not None:
return self.__process(
key_path=key_path,
value=default,
validating_type=validating_type,
permissive_typing=permissive_typing,
depth=depth + 1,
)
elif not is_optional(validating_type):
if depth == 0:
raise MissingRequiredVariable(self, self.pdk)
else:
raise ValueError(f"'{key_path}' must be non-null.")
else:
return None
if is_optional(validating_type):
validating_type = some_of(validating_type)
type_origin = get_origin(validating_type)
type_args = get_args(validating_type)
if type_origin in [list, tuple]:
return_value = list()
raw = value
if isinstance(raw, list) or isinstance(raw, tuple):
# HACK: Allow multiple globs within Path variables
if type_origin is list and type_args == (Path,):
if any(isinstance(item, List) for item in raw):
Variable.__flatten_list(value)
pass # do nothing, can be used as is
elif is_string(raw):
if not permissive_typing:
raise ValueError(
f"Refusing to automatically convert string at '{key_path}' to list"
)
if "," in raw:
raw = raw.split(",")
elif ";" in raw:
raw = raw.split(";")
else:
raw = raw.split()
if len(raw) and raw[-1] == "":
raw.pop() # Trailing commas
else:
raise ValueError(
f"List provided for variable '{key_path}' is invalid: {value}"
)
if type_origin is tuple:
if len(raw) != len(type_args):
raise ValueError(
f"Value provided for variable '{key_path}' of type {validating_type} is invalid: ({len(raw)}/{len(type_args)}) tuple entries provided"
)
for i, (item, value_type) in enumerate(
zip_first(raw, type_args, fillvalue=type_args[0])
):
return_value.append(
self.__process(
key_path=f"{key_path}[{i}]",
value=item,
validating_type=value_type,
permissive_typing=permissive_typing,
depth=depth + 1,
)
)
if type_origin is tuple:
return tuple(return_value)
return return_value
elif type_origin is dict:
raw = value
key_type, value_type = type_args
if isinstance(raw, dict):
pass
elif isinstance(raw, list) or is_string(raw):
if not permissive_typing:
raise ValueError(
f"Refusing to automatically convert string at '{key_path}' to dict"
)
components = raw
if is_string(raw):
components = shlex.split(raw)
assert isinstance(components, list)
# Assuming Tcl format:
if len(components) % 2 != 0:
raise ValueError(
f"Tcl-style flat dictionary provided for variable '{key_path}' is invalid: uneven number of components ({len(components)})"
)
raw = {}
for i in range(0, len(components) // 2):
key = components[2 * i]
val = components[2 * i + 1]
raw[key] = val
else:
raise ValueError(
f"Value provided for variable '{key_path}' of type {validating_type} is invalid: '{value}'"
)
processed = {}
for key, val in raw.items():
key_validated = self.__process(
key_path=key_path,
value=key,
validating_type=key_type,
permissive_typing=permissive_typing,
depth=depth + 1,
)
value_validated = self.__process(
key_path=f"{key_path}.{key_validated}",
value=val,
validating_type=value_type,
permissive_typing=permissive_typing,
depth=depth + 1,
)
processed[key_validated] = value_validated
return processed
elif type_origin == Union:
final_value = None
errors = []
for arg in type_args:
try:
final_value = self.__process(
key_path=key_path,
value=value,
validating_type=arg,
permissive_typing=permissive_typing,
depth=depth + 1,
)
if final_value is not None:
return final_value
except ValueError as e:
errors.append(f"\t{str(e)}")
raise ValueError(
"\n".join(
[
f"Value for '{key_path}' is invalid for union {repr_type(validating_type)}:"
]
+ errors
)
)
elif type_origin == Literal:
if value in type_args:
return value
else:
raise ValueError(
f"Value for '{key_path}' is invalid for {repr_type(validating_type)}: '{value}'"
)
elif is_dataclass(validating_type):
if isinstance(value, validating_type):
# Do not validate further
return value
raw = value
if not isinstance(raw, dict):
raise ValueError(
f"Value provided for deserializable class {validating_type} at '{key_path}' is not a dictionary."
)
raw = value.copy()
kwargs_dict = {}
for current_field in fields(validating_type):
key = current_field.name
subtype: Type[Any] = current_field.type # type: ignore
explicitly_specified = False
if key in raw:
explicitly_specified = True
field_value = raw.get(key)
field_default = None
if (
current_field.default is not None
and current_field.default != MISSING
):
field_default = current_field.default
if current_field.default_factory != MISSING:
field_default = current_field.default_factory()
value__processed = self.__process(
key_path=f"{key_path}.{key}",
value=field_value,
explicitly_specified=explicitly_specified,
default=field_default,
validating_type=subtype, # type: ignore
permissive_typing=permissive_typing,
depth=depth + 1,
)
kwargs_dict[key] = value__processed
if explicitly_specified:
del raw[key]
if len(raw):
raise ValueError(
f"One or more keys unrecognized for dataclass {validating_type.__qualname__}: {' '.join(raw.keys())}"
)
return validating_type(**kwargs_dict)
elif validating_type == Path:
# Handle one-file globs
if isinstance(value, list) and len(value) == 1:
value = value[0]
result = Path(value)
result.validate(f"Path provided for variable '{key_path}' is invalid")
return result
elif validating_type is bool:
if not permissive_typing and not isinstance(value, bool):
raise ValueError(
f"Refusing to automatically convert '{value}' at '{key_path}' to a Boolean"
)
if value in ["1", "true", "True", 1, True]:
return True
elif value in ["0", "false", "False", 0, False]:
return False
else:
raise ValueError(
f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}'"
)
elif issubclass(validating_type, Enum):
if type(value) is validating_type:
return value
try:
return validating_type[value]
except KeyError:
raise ValueError(
f"Variable provided for variable '{key_path}' of enumerated type {validating_type.__name__} is invalid: '{value}'"
)
elif issubclass(validating_type, str):
if not is_string(value):
raise ValueError(
f"Refusing to automatically convert value at '{key_path}' to a string"
)
return str(value)
elif issubclass(validating_type, Decimal) or issubclass(validating_type, int):
try:
final = validating_type(value)
except (InvalidOperation, TypeError):
raise ValueError(
f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}'"
)
if not permissive_typing and not (
isinstance(value, int)
or isinstance(value, float)
or isinstance(value, Decimal)
):
raise ValueError(
f"Refusing to automatically convert value at '{key_path}' to a {validating_type.__name__}"
)
return final
else:
try:
return validating_type(value)
except ValueError as e:
raise ValueError(
f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}' {e}"
)
def compile(
self,
mutable_config: GenericDict[str, Any],
warning_list_ref: List[str],
values_so_far: Optional[Mapping[str, Any]] = None,
permissive_typing: bool = False,
) -> Tuple[Optional[str], Any]:
user_specified_key: Optional[str] = None
value: Optional[Any] = None
i = 0
while (
user_specified_key is None
and self.deprecated_names is not None
and i < len(self.deprecated_names)
):
deprecated_name = self.deprecated_names[i]
deprecated_callable = lambda x: x
if not isinstance(deprecated_name, str):
deprecated_name, deprecated_callable = deprecated_name
user_specified_key, value = mutable_config.check(deprecated_name)
if user_specified_key is not None:
warning_list_ref.append(
f"The configuration variable '{deprecated_name}' is deprecated. Please check the docs for the usage on the replacement variable '{self.name}'."
)
if value is not None:
value = deprecated_callable(value)
i = i + 1
if user_specified_key is None:
user_specified_key, value = mutable_config.check(self.name)
processed = self.__process(
key_path=self.name,
value=value,
default=self.default,
validating_type=self.type,
explicitly_specified=user_specified_key is not None,
permissive_typing=permissive_typing,
)
if user_specified_key is not None:
processed = self.validator(self, processed, warning_list_ref)
return (user_specified_key, processed)
def _get_docs_identifier(self, parent: Optional[str] = None) -> str:
identifier = f"var-{self.name.lower()}"
if parent is not None:
identifier = f"var-{slugify(parent)}-{self.name.lower()}"
return identifier
def __hash__(self) -> int:
return hash((self.name, self.type, self.default))
def __eq__(self, rhs: object) -> bool:
if not isinstance(rhs, Variable):
raise NotImplementedError()
return (
self.name == rhs.name
and self.type == rhs.type
and self.default == rhs.default
)
# Flatten list. Note: Must modify value, not return a new list.
@staticmethod
def __flatten_list(value: list):
new_list = []
for item in value:
if isinstance(item, list):
for sub_item in item:
new_list.append(sub_item)
else:
new_list.append(item)
value[:] = new_list
f24e0ea5dThis file is pinned to the selected release. Download the original to open it in your design tool.