#!/usr/bin/env python3 """Phase 2: Strip names from definition forms in one-per-file .sx files. Transforms: (defcomp ~name (params) body) -> (defcomp (params) body) (define name value) -> (define value) (define (name args) body) -> (define (args) body) (defpage name :path ...) -> (defpage :path ...) etc. Self-named components (where the name is part of the content) are left as-is if they use a different naming convention than the file path would give. Usage: python3 scripts/strip_names.py --dry-run # preview python3 scripts/strip_names.py # execute """ import os import sys import re import argparse sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from shared.sx.parser import parse_all from shared.sx.types import Symbol, Keyword NAMED_DEFS = {"defcomp", "defisland", "defmacro", "defpage", "defhandler", "defstyle", "deftype", "defeffect", "defrelation", "deftest"} SKIP_FILES = {"boundary.sx", "_init.sx", "_preamble.sx"} def strip_name_from_source(source): """Strip the name from a definition in raw source text. Returns (new_source, old_name, def_type) or None if no change needed. Works on raw text to preserve formatting, comments, etc. Uses the parser to understand structure, then does targeted text surgery. """ try: exprs = parse_all(source) except Exception: return None if not exprs: return None expr = exprs[0] if not isinstance(expr, list) or not expr or not isinstance(expr[0], Symbol): return None kw = expr[0].name if kw in NAMED_DEFS: if len(expr) < 2 or not isinstance(expr[1], Symbol): return None # Already unnamed or malformed old_name = expr[1].name # Find and remove the name symbol from source text. # Pattern: (defXXX ~name or (defXXX name # We need to find the name token after the keyword and remove it. pattern = re.compile( r'(\(\s*' + re.escape(kw) + r')\s+' + re.escape(old_name) + r'(?=\s)', re.DOTALL ) match = pattern.search(source) if match: new_source = source[:match.end(1)] + source[match.end():] return (new_source, old_name, kw) return None if kw == "define": if len(expr) < 2: return None name_part = expr[1] if isinstance(name_part, Symbol): old_name = name_part.name # (define name value) -> (define value) pattern = re.compile( r'(\(\s*define)\s+' + re.escape(old_name) + r'(?=\s)', re.DOTALL ) match = pattern.search(source) if match: new_source = source[:match.end(1)] + source[match.end():] return (new_source, old_name, "define") elif (isinstance(name_part, list) and name_part and isinstance(name_part[0], Symbol)): old_name = name_part[0].name # (define (name args...) body) -> (define (args...) body) # Find (define (name and remove just "name " pattern = re.compile( r'(\(\s*define\s+\()\s*' + re.escape(old_name) + r'\s*', re.DOTALL ) match = pattern.search(source) if match: new_source = source[:match.end(1)] + source[match.end():] return (new_source, old_name, "define") return None return None def main(): parser = argparse.ArgumentParser(description="Strip names from definitions") parser.add_argument("--dry-run", action="store_true") parser.add_argument("--dir", default=None) args = parser.parse_args() project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) os.chdir(project_root) dirs = [args.dir] if args.dir else ["sx/sx", "sx/sxc"] stripped = 0 skipped = 0 errors = [] for d in dirs: if not os.path.isdir(d): continue for root, subdirs, files in os.walk(d): subdirs[:] = [x for x in subdirs if x not in ('__pycache__', '.cache')] for filename in sorted(files): if not filename.endswith('.sx') or filename in SKIP_FILES: continue filepath = os.path.join(root, filename) try: with open(filepath, encoding='utf-8') as f: source = f.read() except Exception as e: errors.append((filepath, str(e))) continue result = strip_name_from_source(source) if result is None: skipped += 1 continue new_source, old_name, def_type = result if args.dry_run: print(f" {filepath}: strip {def_type} {old_name}") stripped += 1 continue with open(filepath, 'w', encoding='utf-8') as f: f.write(new_source) stripped += 1 print(f"\n{'Dry run — would strip' if args.dry_run else 'Stripped'} {stripped} names") print(f"Skipped {skipped} (already unnamed or no definition)") if errors: print(f"Errors: {len(errors)}") for fp, e in errors: print(f" {fp}: {e}") if __name__ == "__main__": main()