From c905032a39db900f5b2522572dc8dcf6792f8e78 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 11 Jul 2026 18:46:29 -0600 Subject: [PATCH] Fix DSN tokenizer to accept KiCad net names like /*52 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specctra's SpecCharASCII includes / and *, and an Identifier may start with /, so KiCad emits hierarchical net names such as /*52 and /53. The tokenizer treated any /* as a block-comment start and raised 'unterminated comment' when no */ followed — rejecting real KiCad DSN. Match FreeRouting's JFlex rule-order resolution: /* is a comment only when a closing */ exists; otherwise it is an ordinary name run. Validated against a KiCad 10.0.4 pcbnew-exported DSN (78 nets incl. /*52, /53). --- src/freeroute/dsn/tokenizer.py | 18 ++++++++++++------ tests/dsn/test_tokenizer.py | 13 ++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/freeroute/dsn/tokenizer.py b/src/freeroute/dsn/tokenizer.py index 1238a53..a671dc2 100644 --- a/src/freeroute/dsn/tokenizer.py +++ b/src/freeroute/dsn/tokenizer.py @@ -126,13 +126,19 @@ def tokenize(text: str, quote_chars: str = _QUOTES) -> list[Token]: eol = text.find("\n", i) i = n if eol == -1 else eol continue - if ch == "/" and i + 1 < n and text[i + 1] == "*": # block comment + # `/* ... */` is a block comment ONLY when a closing `*/` exists. + # SpecCharASCII includes `/` and `*`, and an Identifier may start with + # `/`, so KiCad emits net names like `/*52`. FreeRouting's JFlex scanner + # resolves the ambiguity by longest-match / rule-order: with no closing + # `*/`, the comment rule fails and the run is read as a name. Mirror + # that — an unclosed `/*` is an ordinary token, not an error. + if ch == "/" and i + 1 < n and text[i + 1] == "*": end = text.find("*/", i + 2) - if end == -1: - raise DsnSyntaxError(f"unterminated /* */ comment at line {line}") - line += text.count("\n", i, end) - i = end + 2 - continue + if end != -1: + line += text.count("\n", i, end) + i = end + 2 + continue + # no closing `*/` — fall through and read `/*...` as a normal token # Brackets ----------------------------------------------------------- if ch == "(": diff --git a/tests/dsn/test_tokenizer.py b/tests/dsn/test_tokenizer.py index f6bb8f4..cb01314 100644 --- a/tests/dsn/test_tokenizer.py +++ b/tests/dsn/test_tokenizer.py @@ -117,9 +117,16 @@ def test_unterminated_string_raises(): tokenize('(a "no end') -def test_unterminated_block_comment_raises(): - with pytest.raises(DsnSyntaxError): - tokenize("(a /* no end") +def test_unclosed_block_comment_is_name_token(): + # KiCad emits hierarchical net names like `/*52` (SpecCharASCII includes + # `/` and `*`). With no closing `*/`, `/*...` is a name run, not an + # unterminated comment — matching FreeRouting's JFlex rule-order resolution. + assert texts("(net /*52)") == ["(", "net", "/*52", ")"] + + +def test_closed_block_comment_is_stripped(): + # A properly closed `/* ... */` is still a comment and gets dropped. + assert texts("(a /* c */ b)") == ["(", "a", "b", ")"] def test_hyphenated_pin_ref_is_single_atom():