The router began as a port of FreeRouting and has grown its own engine (negotiated congestion, tree nets, exact checks, axis escapes), and 'freeroute' was one letter from the project it no longer follows. The package, module and command are now rpmroute; the freeroute command stays as an alias, and the RPMROUTE / RPMROUTE_ORACLE_JAVA variables fall back to the old FREEROUTE names. Version 2026.10.07. Routing is unchanged: a PCBench board routes to a byte-identical session either way.
104 lines
3.2 KiB
Python
104 lines
3.2 KiB
Python
"""Indented S-expression output and identifier quoting for SES writing.
|
|
|
|
Ports two FreeRouting helpers used by the session writer:
|
|
|
|
* :class:`IndentWriter` mirrors ``datastructures/IndentFileWriter`` — a 2-space
|
|
indenting writer with ``start_scope`` / ``end_scope`` / ``new_line``.
|
|
* :class:`Identifier` mirrors ``datastructures/IdentifierType`` — quotes a name
|
|
when it contains a reserved character, a non-ASCII character, or starts with a
|
|
digit; strips surrounding quotes and any embedded quote character first.
|
|
|
|
The SES reserved-character set is the one ``SesWriter`` constructs:
|
|
``( ) <space> ; - _ / ~ { }``. Note that ``-`` and ``_`` are reserved, so net
|
|
names like ``NET_A`` and padstacks like ``Via[0-1]_600:300_um`` are quoted.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from io import StringIO
|
|
import re
|
|
|
|
__all__ = ["IndentWriter", "Identifier", "SES_RESERVED_CHARS"]
|
|
|
|
_INDENT = " "
|
|
|
|
#: Reserved characters for SES identifiers (from SesWriter.write).
|
|
SES_RESERVED_CHARS: tuple[str, ...] = (
|
|
"(",
|
|
")",
|
|
" ",
|
|
";",
|
|
"-",
|
|
"_",
|
|
"/",
|
|
"~",
|
|
"{",
|
|
"}",
|
|
)
|
|
|
|
_LEADING_DIGIT = re.compile(r"-?\d")
|
|
|
|
|
|
class IndentWriter:
|
|
"""Accumulates indented S-expression text, mirroring ``IndentFileWriter``."""
|
|
|
|
def __init__(self) -> None:
|
|
self._buf = StringIO()
|
|
self._level = 0
|
|
|
|
def write(self, text: str) -> None:
|
|
self._buf.write(text)
|
|
|
|
def new_line(self) -> None:
|
|
"""Start a new line at the current indent level."""
|
|
self._buf.write("\n")
|
|
self._buf.write(_INDENT * self._level)
|
|
|
|
def start_scope(self, new_line: bool = True) -> None:
|
|
"""Open a ``(`` scope, optionally on a fresh line, and indent."""
|
|
if new_line:
|
|
self.new_line()
|
|
self._buf.write("(")
|
|
self._level += 1
|
|
|
|
def end_scope(self) -> None:
|
|
"""Close the innermost scope with ``)`` on its own aligned line."""
|
|
self._level -= 1
|
|
self.new_line()
|
|
self._buf.write(")")
|
|
|
|
def getvalue(self) -> str:
|
|
return self._buf.getvalue()
|
|
|
|
|
|
class Identifier:
|
|
"""Formats a name as a DSN/SES token, quoting it when required."""
|
|
|
|
def __init__(
|
|
self, string_quote: str = '"', reserved: tuple[str, ...] = SES_RESERVED_CHARS
|
|
) -> None:
|
|
self.quote = string_quote or '"'
|
|
self.reserved = tuple(reserved)
|
|
|
|
def format(self, name: str) -> str:
|
|
"""Return ``name`` quoted if it needs quoting, else unchanged."""
|
|
# Strip a surrounding matching pair of quote characters.
|
|
while len(name) >= 2 and name[0] == self.quote and name[-1] == self.quote:
|
|
name = name[1:-1]
|
|
# Remove any embedded quote character (it cannot be escaped).
|
|
if self.quote in name:
|
|
name = name.replace(self.quote, "")
|
|
|
|
need_quotes = any(rc in name for rc in self.reserved)
|
|
if not need_quotes and any(ord(ch) > 127 for ch in name):
|
|
need_quotes = True
|
|
if not need_quotes and _LEADING_DIGIT.match(name):
|
|
need_quotes = True
|
|
|
|
if need_quotes:
|
|
return f"{self.quote}{name}{self.quote}"
|
|
return name
|
|
|
|
def write(self, name: str, out: IndentWriter) -> None:
|
|
out.write(self.format(name))
|