diff --git a/README.md b/README.md index 49019bf..48678a9 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,13 @@ HAS bridges the gap between high-level languages and assembly programming. It of ### Advanced Features -- **Macro System (Phase 2)**: Define reusable code patterns -- **@python Directive (Phase 4)**: Execute Python code during compilation +- **Macro System**: Define reusable code patterns +- **@python Directive**: Execute Python code during compilation - **Include System**: Modular code organization with `#include` - **Constants**: Compile-time constant evaluation - **Pointer Arithmetic**: Address-of (`&`) and dereference (`*`) operators - **Register Locking**: `#pragma lockreg()` to protect registers from compiler allocation +- **Dead-Code Elimination**: `--strip-unused-procs` removes unreachable internal procedures before assembly emission ### Amiga-Specific @@ -188,7 +189,7 @@ These examples demonstrate game-related concepts and systems, but they are not r ### Code Generation -- `code_generator.py` - External Python code generation (Phase 1) +- `code_generator.py` - External Python code generation - `simple_generator.py` - Simple generation example ## 🔧 Usage @@ -199,7 +200,7 @@ These examples demonstrate game-related concepts and systems, but they are not r python -m hasc.cli input.has -o output.s ``` -### With External Code Generation (Phase 1) +### With External Code Generation ```bash python -m hasc.cli main.has --generate generator.py -o output.s @@ -225,6 +226,29 @@ if __name__ == "__main__": python -m hasc.cli input.has --no-validate -o output.s ``` +### Remove Unused Procedures (dead-code elimination) + +```bash +# Remove unreachable internal procedures before assembly emission +python -m hasc.cli program.has --strip-unused-procs -o program.s + +# Same, but also print what was removed +python -m hasc.cli program.has --strip-unused-procs --strip-unused-report -o program.s +``` + +The pass uses call-graph reachability from `public` declarations. It is +**conservative by default**: if no roots are found, or if a top-level raw +`asm` block is present, all procedures are kept unchanged. + +```has +// Mark the entry point so unreachable procs can be stripped +public game_init; + +proc game_init() -> void { ... } // kept (root) +proc helper() -> void { ... } // kept (called by game_init) +proc dead_code() -> void { ... } // removed (never called) +``` + ### Build Complete Executable ```bash diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b67d5d2..07a3242 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to the HAS (High Assembler) project will be documented in th ### Added +- **Dead-procedure elimination pass** (`--strip-unused-procs` / `--strip-unused-report`): + - New module `hasc/reachability.py` performs conservative call-graph analysis after validation and before code generation. + - Roots are discovered from `public` declarations that point to internal `proc` definitions. + - Unreachable internal procedures are removed from the AST before assembly is emitted. + - Three conservative keep-all safeguards prevent incorrect stripping: + - **Feature off by default** — requires an explicit opt-in flag. + - **Top-level asm block** — raw `jsr`/`jmp` may reference any label; all procs kept. + - **No roots found** — keeps everything rather than silently discarding all code. + - `--strip-unused-report` prints roots, kept, and removed procedure lists to stderr. + - Three new example files demonstrate all scenarios: `strip_unused_procs_demo.has`, `strip_unused_procs_asm_safe.has`, `strip_unused_procs_no_roots.has`. + - **Example suite split gate** for deterministic regression checks: - Added `examples/negative_examples.txt` manifest for expected-fail examples. - Added `scripts/test_examples_split.sh` to enforce: @@ -163,9 +174,9 @@ All notable changes to the HAS (High Assembler) project will be documented in th - Increment/Decrement: ++, -- #### Advanced Features -- **Phase 1**: External Python code generation via `--generate` flag -- **Phase 2**: Macro system with parameter substitution -- **Phase 3**: Inline Python execution with `@python` directive +- External Python code generation via `--generate` flag +- Macro system with parameter substitution +- Inline Python execution with `@python` directive - Include system with cyclic dependency detection - Inline assembly support with `asm { }` blocks - Register manipulation with `getreg()` and `setreg()` intrinsics diff --git a/docs/COMPILER_DEVELOPERS_GUIDE.md b/docs/COMPILER_DEVELOPERS_GUIDE.md index 46d7d5c..b0a9f50 100644 --- a/docs/COMPILER_DEVELOPERS_GUIDE.md +++ b/docs/COMPILER_DEVELOPERS_GUIDE.md @@ -55,6 +55,8 @@ src/hasc/ ↓ [Validator] ───→ Validated AST + Warnings ↓ + [Reachability] → Pruned AST (opt-in; see --strip-unused-procs) + ↓ [CodeGen] ─────→ 68000 Assembly Text ↓ vasm/vlink ───→ Executable Binary @@ -65,6 +67,7 @@ src/hasc/ - Sections: `data`/`data_chip`, `bss`/`bss_chip`, `code`/`code_chip`, inline `asm` - Compile-time directives: `#warning`, `#error`, `#pragma lockreg(...)`, `#pragma strict16arith(on|off)`, `const` declarations +- Dead-code elimination: `--strip-unused-procs` removes unreachable internal `proc` definitions via call-graph analysis (`hasc/reachability.py`) - Procedure system: `proc`, forward `func` declarations, `extern func/var`, `public` exports - Control flow and expressions: loops (`for`/`while`/`repeat`), conditionals, full operator set including shifts and bitwise ops - Python integration: macros, `@python` directives, optional external generation via `--generate` @@ -1322,7 +1325,7 @@ When working on the compiler, always: ### Internal Documentation - [README.md](README.md) - Project overview -- [QUICK_START_ALL_PHASES.md](QUICK_START_ALL_PHASES.md) - Phase implementation guide +- [QUICK_START_ALL_PHASES.md](QUICK_START_ALL_PHASES.md) - Implementation guide - [OPERATORS.md](OPERATORS.md) - Operator precedence and implementation - [PROC_VS_FUNC_SUMMARY.md](PROC_VS_FUNC_SUMMARY.md) - Function declaration patterns diff --git a/docs/DEVELOPERS_GUIDE.md b/docs/DEVELOPERS_GUIDE.md index 1d86283..339149c 100644 --- a/docs/DEVELOPERS_GUIDE.md +++ b/docs/DEVELOPERS_GUIDE.md @@ -756,7 +756,7 @@ code increment: ## Advanced Features -### Macros (Phase 2) +### Macros ```has ; Define reusable code patterns macro load_register(reg, value) { @@ -775,7 +775,7 @@ code macro_demo: } ``` -### Python Directives (Phase 3) +### Python Directives ```has code python_demo: proc computed() -> long { @@ -790,7 +790,7 @@ code python_demo: } ``` -### External Code Generation (Phase 1) +### External Code Generation Create `generator.py`: ```python #!/usr/bin/env python3 diff --git a/docs/PYTHON_GENERATION_TUTORIAL.md b/docs/PYTHON_GENERATION_TUTORIAL.md index 698188d..c49673b 100644 --- a/docs/PYTHON_GENERATION_TUTORIAL.md +++ b/docs/PYTHON_GENERATION_TUTORIAL.md @@ -429,13 +429,13 @@ with open('/tmp/debug.has', 'w') as f: 1. Try running the example: `python3 examples/code_generator.py` 2. Create your own generator for a specific use case 3. Integrate into your build process -4. Consider implementing Phase 2 (macros) if needed +4. Consider adding macros if needed --- -## What's Next: Phase 2 +## What's Next -- **Phase 2**: `@macro` directives for repetitive patterns -- **Phase 3**: `@python` inline code blocks (requires sandbox) +- `@macro` directives for repetitive patterns +- `@python` inline code blocks (requires sandbox) -For now, Phase 1 (external Python) gives you full power with no changes to the compiler core! +For now, external Python generation gives you full power with no changes to the compiler core. diff --git a/docs/PYTHON_INTEGRATION.md b/docs/PYTHON_INTEGRATION.md index 930853b..179b2bc 100644 --- a/docs/PYTHON_INTEGRATION.md +++ b/docs/PYTHON_INTEGRATION.md @@ -271,18 +271,18 @@ if args.generate: ## Recommended Implementation Path -### Phase 1: External Python (Quick Win) +### External Python (Quick Win) - Implement `--generate` CLI option - Use subprocess to run Python scripts - Allows users to generate HAS files before compilation - No changes to compiler needed -### Phase 2: Macro System (Foundation) +### Macro System (Foundation) - Add macro definitions to grammar - Implement macro expansion - Perfect for repetitive patterns -### Phase 3: Python Sandbox (Advanced) +### Python Sandbox (Advanced) - Add `@python` directive - Implement safe execution sandbox - Full compile-time code generation @@ -290,7 +290,7 @@ if args.generate: --- -## Detailed Implementation: Phase 1 (Start Here) +## Detailed Implementation: External Python (Start Here) ### 1. Modify CLI @@ -446,7 +446,7 @@ for op in ['add', 'sub', 'mul']: ## Recommendation -**Start with Phase 1 (External Python):** +**Start with external Python integration:** 1. **Implement `--generate` option** in CLI (30 minutes) - Add argument parsing @@ -467,7 +467,7 @@ for op in ['add', 'sub', 'mul']: - ✅ Minimal changes to HAS core - ✅ Full Python power immediately - ✅ Easy for users to understand -- ✅ Can extend to Phases 2-4 later +- ✅ Can extend with macro and sandbox features later - ✅ No security sandbox needed - ✅ Can debug generated code easily @@ -477,7 +477,7 @@ Then later add `@macro` directive as needed. ## Next Steps -1. Would you like me to implement Phase 1 (`--generate` option)? +1. Would you like me to implement the `--generate` option? 2. Should we add `@macro` support alongside? 3. Any specific use case you have in mind? diff --git a/docs/STRUCT_POINTER_IMPLEMENTATION.md b/docs/STRUCT_POINTER_IMPLEMENTATION.md index f799514..8d3c2c7 100644 --- a/docs/STRUCT_POINTER_IMPLEMENTATION.md +++ b/docs/STRUCT_POINTER_IMPLEMENTATION.md @@ -48,7 +48,7 @@ var x = (*p).x; // Read member through pointer ## Implementation Plan -### Phase 1: Type System Enhancement +### Type System Enhancement #### 1.1 Update ast.py @@ -83,7 +83,7 @@ def pointer_base_type(typename: str) -> Optional[str]: return None ``` -### Phase 2: Parser Updates +### Parser Updates The parser already supports pointer types in variable declarations through the `type` rule. However, the `lvalue` grammar rule needs to be extended to support dereferenced struct member access. @@ -147,7 +147,7 @@ def lvalue(self, items): # ... rest of existing code ``` -### Phase 3: Semantic Validation +### Semantic Validation Add validation to ensure struct types are valid. @@ -187,7 +187,7 @@ def _validate_data_section(self, section): self.struct_types.add(item.name) ``` -### Phase 4: Code Generation +### Code Generation The code generator already handles: 1. Address-of for array elements: `&bullet[i]` @@ -246,7 +246,7 @@ Ensure `vtype` is stored as the string `"bullet*"` (not processed or stripped). Looking at the code, the VarDecl AST node has a `vtype` field that should contain the full type string including the `*`. -### Phase 5: Testing +### Testing #### 5.1 Create Test File diff --git a/examples/games/caveride/Makefile b/examples/games/caveride/Makefile index 32186fa..d6493bd 100644 --- a/examples/games/caveride/Makefile +++ b/examples/games/caveride/Makefile @@ -26,6 +26,7 @@ VASM := vasmm68k_mot VLINK := vlink WHICH := which HASC := $(PYTHON) -m hasc.cli +HASC_FLAGS := --strip-unused-procs --strip-unused-report # Sources and targets # Main has file (must be first for linker) @@ -52,6 +53,13 @@ ASSET_OBJS := $(ASSET_SRCS:$(SRC_DIR)/%.s=$(BUILD_DIR)/%.o) VASM_FLAGS := -m68000 -Fhunk -kick1hunks -I$(LIB_DIR) VLINK_FLAGS := -bamigahunk -Bstatic +# Strip symbols from the final executable by default. +# Use STRIP=0 for debug builds that keep symbols. +STRIP ?= 1 +ifeq ($(STRIP),1) + VLINK_FLAGS += -s +endif + .PHONY: all clean compile assemble link run help libs game-objs asset-objs all: $(BUILD_DIR) $(EXE_FILE) @@ -75,7 +83,7 @@ $(BUILD_DIR)/%.o: %.has | $(BUILD_DIR) echo "Error: Python interpreter '$(PYTHON)' not found. Set up venv or install python3."; \ exit 1; \ fi - PYTHONPATH="$(PYTHONPATH)" "$(PYTHON)" -m hasc.cli "$(abspath $<)" -o "$(abspath $(BUILD_DIR)/$*.s)" + PYTHONPATH="$(PYTHONPATH)" $(HASC) $(HASC_FLAGS) "$(abspath $<)" -o "$(abspath $(BUILD_DIR)/$*.s)" $(VASM) $(VASM_FLAGS) -L $(BUILD_DIR)/$*.lst -o $@ $(BUILD_DIR)/$*.s $(MAIN_OBJ): $(MAIN_HAS) | $(BUILD_DIR) @@ -104,6 +112,8 @@ asset-objs: $(ASSET_OBJS) help: @echo "Targets: all (default), compile, assemble, link, libs, clean, run, help" + @echo "Options: STRIP=1 (default, strips symbols), STRIP=0 (keep symbols)" + @echo "Compiler flags: HASC_FLAGS='$(HASC_FLAGS)' (override to customize)" clean: rm -rf $(BUILD_DIR)/*.o $(BUILD_DIR)/*.s $(BUILD_DIR)/*.exe $(BUILD_DIR)/*.lst $(BUILD_DIR)/assets/ diff --git a/examples/strip_unused_procs_asm_safe.has b/examples/strip_unused_procs_asm_safe.has new file mode 100644 index 0000000..b9edf67 --- /dev/null +++ b/examples/strip_unused_procs_asm_safe.has @@ -0,0 +1,35 @@ +// strip_unused_procs_asm_safe.has +// Demonstrates the conservative asm-block safeguard in --strip-unused-procs. +// +// When a top-level raw asm block is present in a code section the compiler +// cannot safely prove which internal procedures are reachable via jsr/jmp +// in that block. The pass therefore keeps ALL internal procedures. +// +// Compile with stripping: +// python -m hasc.cli examples/strip_unused_procs_asm_safe.has \ +// --strip-unused-procs --strip-unused-report -o /tmp/asm_safe.s +// → "top-level asm block detected; keeping all internal procedures" +// → labels: startup, helper, unreachable_from_has (all kept) + +code demo: + // Top-level asm block that calls internal code via raw jsr. + // The compiler cannot statically trace these targets, so it keeps all procs. + asm { + jsr startup + rts + } + + proc startup() -> void { + var r: int = helper(7); + return; + } + + proc helper(x: int) -> int { + return x + 1; + } + + // Called from raw asm above (not via HAS call statement). + // Without the asm-block safeguard this would be incorrectly removed. + proc unreachable_from_has() -> int { + return 0; + } diff --git a/examples/strip_unused_procs_demo.has b/examples/strip_unused_procs_demo.has new file mode 100644 index 0000000..92ae4b6 --- /dev/null +++ b/examples/strip_unused_procs_demo.has @@ -0,0 +1,40 @@ +// strip_unused_procs_demo.has +// Demonstrates --strip-unused-procs (dead internal procedure removal). +// +// Root: public entry_point; +// Reachable chain: entry_point → compute → helper +// Dead (never called): dead_a, dead_b +// +// Compile without stripping: +// python -m hasc.cli examples/strip_unused_procs_demo.has -o /tmp/demo.s +// → labels: entry_point, compute, helper, dead_a, dead_b +// +// Compile with stripping: +// python -m hasc.cli examples/strip_unused_procs_demo.has \ +// --strip-unused-procs --strip-unused-report -o /tmp/demo.s +// → Strip report removed: dead_a, dead_b +// → labels: entry_point, compute, helper (dead procs absent) + +code demo: + public entry_point; + + proc entry_point() -> int { + return compute(10, 32); + } + + proc compute(a: int, b: int) -> int { + return helper(a) + b; + } + + proc helper(x: int) -> int { + return x * 2; + } + + // These procedures are never called from any reachable path. + proc dead_a() -> int { + return 99; + } + + proc dead_b() -> int { + return dead_a(); + } diff --git a/examples/strip_unused_procs_no_roots.has b/examples/strip_unused_procs_no_roots.has new file mode 100644 index 0000000..926ef74 --- /dev/null +++ b/examples/strip_unused_procs_no_roots.has @@ -0,0 +1,32 @@ +// strip_unused_procs_no_roots.has +// Demonstrates the conservative "no roots → keep all" safeguard in +// --strip-unused-procs. +// +// When a code section contains only proc definitions with no public export +// and no top-level asm block, the compiler cannot determine an entry point +// and keeps every procedure to avoid silent data loss. +// +// Compile with stripping: +// python -m hasc.cli examples/strip_unused_procs_no_roots.has \ +// --strip-unused-procs --strip-unused-report -o /tmp/no_roots.s +// → Strip report roots: +// → Strip report kept: alpha, beta, gamma (all kept — no roots found) +// → Strip report removed: +// +// To make the pass actually strip, add: +// public alpha; +// and recompile — then beta and gamma will be removed. + +code library: + + proc alpha() -> int { + return 1; + } + + proc beta() -> int { + return 2; + } + + proc gamma() -> int { + return alpha() + beta(); + } diff --git a/hasc/cli.py b/hasc/cli.py index 6233f02..0418c39 100644 --- a/hasc/cli.py +++ b/hasc/cli.py @@ -2,6 +2,7 @@ import sys import subprocess from . import parser, codegen, validator +from . import reachability import os from lark.exceptions import LarkError, UnexpectedInput, UnexpectedToken, UnexpectedCharacters @@ -33,6 +34,16 @@ def main(argv=None): ap.add_argument("--version", action="version", version=f"%(prog)s {__version__}") ap.add_argument("--generate", help="Pre-process with Python script to generate code") ap.add_argument("--no-validate", action="store_true", help="Skip validation checks") + ap.add_argument( + "--strip-unused-procs", + action="store_true", + help="Remove unreachable internal procedures before code generation", + ) + ap.add_argument( + "--strip-unused-report", + action="store_true", + help="Print kept/removed procedure report (implies --strip-unused-procs)", + ) args = ap.parse_args(argv) # If --generate specified, run Python script to generate HAS code @@ -103,6 +114,22 @@ def main(argv=None): print(f" {e}", file=sys.stderr) sys.exit(1) + strip_enabled = args.strip_unused_procs or args.strip_unused_report + mod, strip_report = reachability.strip_unused_procs(mod, enabled=strip_enabled) + + if args.strip_unused_report: + if strip_report.skipped_due_to_asm: + print( + "Strip report: top-level asm block detected; keeping all internal procedures", + file=sys.stderr, + ) + roots = ", ".join(strip_report.roots) if strip_report.roots else "" + kept = ", ".join(strip_report.reachable) if strip_report.reachable else "" + removed = ", ".join(strip_report.removed) if strip_report.removed else "" + print(f"Strip report roots: {roots}", file=sys.stderr) + print(f"Strip report kept: {kept}", file=sys.stderr) + print(f"Strip report removed: {removed}", file=sys.stderr) + cg = codegen.CodeGen(mod) try: asm = cg.gen() diff --git a/hasc/reachability.py b/hasc/reachability.py new file mode 100644 index 0000000..a74ab31 --- /dev/null +++ b/hasc/reachability.py @@ -0,0 +1,240 @@ +from dataclasses import dataclass +from typing import List, Set, Tuple + +from . import ast + + +@dataclass +class StripReport: + enabled: bool + skipped_due_to_asm: bool + roots: List[str] + reachable: List[str] + removed: List[str] + + +def _collect_internal_procs(module: ast.Module) -> Set[str]: + procs: Set[str] = set() + for item in module.items: + if isinstance(item, ast.CodeSection): + for code_item in item.items: + if isinstance(code_item, ast.Proc): + procs.add(code_item.name) + return procs + + +def _collect_direct_calls_from_expr(expr, out_calls: Set[str]) -> None: + if expr is None: + return + + if isinstance(expr, ast.Call): + out_calls.add(expr.name) + for arg in expr.args: + _collect_direct_calls_from_expr(arg, out_calls) + return + + if isinstance(expr, ast.BinOp): + _collect_direct_calls_from_expr(expr.left, out_calls) + _collect_direct_calls_from_expr(expr.right, out_calls) + return + + if isinstance(expr, ast.UnaryOp): + _collect_direct_calls_from_expr(expr.operand, out_calls) + return + + if isinstance(expr, ast.ArrayAccess): + for idx in expr.indices: + _collect_direct_calls_from_expr(idx, out_calls) + return + + if isinstance(expr, ast.MemberAccess): + _collect_direct_calls_from_expr(expr.base, out_calls) + return + + if isinstance(expr, ast.SetReg): + _collect_direct_calls_from_expr(expr.value, out_calls) + return + + # Prefix/postfix operators can wrap expressions with calls. + if isinstance(expr, (ast.PreIncr, ast.PreDecr, ast.PostIncr, ast.PostDecr)): + _collect_direct_calls_from_expr(expr.operand, out_calls) + return + + +def _collect_direct_calls_from_stmt(stmt, out_calls: Set[str]) -> None: + if isinstance(stmt, ast.CallStmt): + out_calls.add(stmt.name) + for arg in stmt.args: + _collect_direct_calls_from_expr(arg, out_calls) + return + + if isinstance(stmt, ast.MacroCall): + # MacroCall can be either a macro expansion or an implicit function call. + out_calls.add(stmt.name) + for arg in stmt.args: + _collect_direct_calls_from_expr(arg, out_calls) + return + + if isinstance(stmt, ast.VarDecl): + _collect_direct_calls_from_expr(stmt.init_expr, out_calls) + return + + if isinstance(stmt, ast.Assign): + _collect_direct_calls_from_expr(stmt.expr, out_calls) + _collect_direct_calls_from_expr(stmt.target, out_calls) + return + + if isinstance(stmt, ast.CompoundAssign): + _collect_direct_calls_from_expr(stmt.expr, out_calls) + return + + if isinstance(stmt, ast.Return): + _collect_direct_calls_from_expr(stmt.expr, out_calls) + return + + if isinstance(stmt, ast.ExprStmt): + _collect_direct_calls_from_expr(stmt.expr, out_calls) + return + + if isinstance(stmt, ast.If): + _collect_direct_calls_from_expr(stmt.cond, out_calls) + for s in stmt.then_body: + _collect_direct_calls_from_stmt(s, out_calls) + for s in (stmt.else_body or []): + _collect_direct_calls_from_stmt(s, out_calls) + return + + if isinstance(stmt, ast.While): + _collect_direct_calls_from_expr(stmt.cond, out_calls) + for s in stmt.body: + _collect_direct_calls_from_stmt(s, out_calls) + return + + if isinstance(stmt, ast.DoWhile): + for s in stmt.body: + _collect_direct_calls_from_stmt(s, out_calls) + _collect_direct_calls_from_expr(stmt.cond, out_calls) + return + + if isinstance(stmt, ast.ForLoop): + _collect_direct_calls_from_expr(stmt.start, out_calls) + _collect_direct_calls_from_expr(stmt.end, out_calls) + _collect_direct_calls_from_expr(stmt.step, out_calls) + for s in stmt.body: + _collect_direct_calls_from_stmt(s, out_calls) + return + + if isinstance(stmt, ast.RepeatLoop): + _collect_direct_calls_from_expr(stmt.count, out_calls) + for s in stmt.body: + _collect_direct_calls_from_stmt(s, out_calls) + return + + +def _build_call_graph(module: ast.Module, internal_procs: Set[str]) -> Tuple[dict, Set[str], bool]: + graph = {name: set() for name in internal_procs} + roots: Set[str] = set() + has_top_level_asm = False + + for item in module.items: + if not isinstance(item, ast.CodeSection): + continue + + for code_item in item.items: + if isinstance(code_item, ast.Proc): + proc_calls: Set[str] = set() + for stmt in code_item.body: + _collect_direct_calls_from_stmt(stmt, proc_calls) + graph[code_item.name] = set(c for c in proc_calls if c in internal_procs) + elif isinstance(code_item, ast.PublicDecl): + if code_item.name in internal_procs: + roots.add(code_item.name) + elif isinstance(code_item, ast.CallStmt): + if code_item.name in internal_procs: + roots.add(code_item.name) + elif isinstance(code_item, ast.MacroCall): + if code_item.name in internal_procs: + roots.add(code_item.name) + elif isinstance(code_item, ast.AsmBlock): + has_top_level_asm = True + + return graph, roots, has_top_level_asm + + +def strip_unused_procs(module: ast.Module, enabled: bool = False): + if not enabled: + report = StripReport( + enabled=False, + skipped_due_to_asm=False, + roots=[], + reachable=[], + removed=[], + ) + return module, report + + internal_procs = _collect_internal_procs(module) + if not internal_procs: + report = StripReport( + enabled=True, + skipped_due_to_asm=False, + roots=[], + reachable=[], + removed=[], + ) + return module, report + + graph, roots, has_top_level_asm = _build_call_graph(module, internal_procs) + + # Conservative policy: top-level raw assembly can jump to internal procedures. + # Also keep everything when no explicit roots were discovered. + if has_top_level_asm or not roots: + reachable = set(internal_procs) + else: + reachable: Set[str] = set() + work = list(roots) + while work: + current = work.pop() + if current in reachable: + continue + reachable.add(current) + for callee in graph.get(current, set()): + if callee not in reachable: + work.append(callee) + + removed = sorted(list(internal_procs - reachable)) + reachable_sorted = sorted(list(reachable)) + roots_sorted = sorted(list(roots)) + + if not removed: + report = StripReport( + enabled=True, + skipped_due_to_asm=has_top_level_asm, + roots=roots_sorted, + reachable=reachable_sorted, + removed=[], + ) + return module, report + + new_items = [] + for item in module.items: + if not isinstance(item, ast.CodeSection): + new_items.append(item) + continue + + filtered = [] + for code_item in item.items: + if isinstance(code_item, ast.Proc) and code_item.name in removed: + continue + filtered.append(code_item) + + new_items.append(ast.CodeSection(name=item.name, is_chip=item.is_chip, items=filtered)) + + new_module = ast.Module(items=new_items) + report = StripReport( + enabled=True, + skipped_due_to_asm=has_top_level_asm, + roots=roots_sorted, + reachable=reachable_sorted, + removed=removed, + ) + return new_module, report