From dbce55256289d0b747695be8ba003ee997f0d7bd Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Tue, 29 Sep 2026 18:20:41 +0800 Subject: [PATCH] fix: any() in results picks the first alternative; multi-char sets stay alternations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A to-position any() lists alternative spellings; the Ruby runtime picks the first in non-iterating mode — _repl_of now resolves the first element (nested anys and none included). _render_item collapsed all-string sets into a single-char class (any("helloHelloHELLO") matches every character); sets now keep the list form unless every string is a single character. title_case keeps its separator: the after-separator upcasing used a consuming match, deleting every separator occurrence. --- src/interscript/engine.py | 2 +- src/interscript/isc.py | 10 +++++++++- tests/test_isc.py | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/interscript/engine.py b/src/interscript/engine.py index 39c7c00..0d4afad 100644 --- a/src/interscript/engine.py +++ b/src/interscript/engine.py @@ -176,7 +176,7 @@ def _run_op(self, op: dict, text: str) -> str: ) if sep != "": out = re.sub( - re.escape(sep) + "(.)", + f"(?<={re.escape(sep)})(.)", lambda m: m.group(1).upper(), out, ) diff --git a/src/interscript/isc.py b/src/interscript/isc.py index 177e30d..ff132c5 100644 --- a/src/interscript/isc.py +++ b/src/interscript/isc.py @@ -655,7 +655,9 @@ def _render_item(item: dict, aliases: dict[str, str]) -> str: return " + ".join(_render_item(part, aliases) for part in item["parts"]) if kind == "set": items = item["items"] - if all(sub["type"] == "string" for sub in items): + if all(sub["type"] == "string" for sub in items) and all( + len(sub["value"]) == 1 for sub in items + ): return f'any("{_escape("".join(sub["value"] for sub in items))}")' return "any([" + ", ".join(_render_item(sub, aliases) for sub in items) + "])" if kind == "range": @@ -751,6 +753,12 @@ def _repl_of(item: dict, aliases: dict[str, str]) -> str: return "".join(_repl_of(part, aliases) for part in item["parts"]) if kind == "capture": return f"${item['index']}" + if kind == "set": + # A to-position any() lists alternative spellings; the Ruby + # runtime picks the first in non-iterating mode. + if not item["items"]: + return "" + return _repl_of(item["items"][0], aliases) if kind == "alias_ref": name = item["name"] if name not in aliases: diff --git a/tests/test_isc.py b/tests/test_isc.py index b8d715d..e39095e 100644 --- a/tests/test_isc.py +++ b/tests/test_isc.py @@ -192,3 +192,22 @@ def test_title_case_word_separator(): "kind": "title_case", "word_separator": "", } + + +def test_any_in_result_picks_first_alternative(): + src = 'system "x" { stage main { sub any("ab") any(["XA", "YB"]) } }' + tree = isc_to_tree(src) + subst = tree["stages"][0]["children"][0] + assert subst["result"] == "XA" + + +def test_any_in_result_nested_first(): + src = 'system "x" { stage main { sub "a" any([any("XY"), "Z"]) } }' + tree = isc_to_tree(src) + assert tree["stages"][0]["children"][0]["result"] == "X" + + +def test_any_in_result_none_first(): + src = 'system "x" { stage main { sub "a" any([none, " "]) } }' + tree = isc_to_tree(src) + assert tree["stages"][0]["children"][0]["result"] == ""