phase6: reproduce a gp-relative function and record the small-data evidence

This commit is contained in:
Christopher Williams
2026-09-23 21:09:07 -04:00
parent 4688662cf4
commit d379b84ec3
9 changed files with 315 additions and 19 deletions
+1
View File
@@ -13,3 +13,4 @@
0x80017AD4 0x80017AE8 src/func_80017AD4.c
0x8002D2A0 0x8002D2BC src/func_8002D2A0.c
0x8002D2BC 0x8002D2D4 src/func_8002D2BC.c
0x80012780 0x8001278C src/func_80012780.c
1 # Code-region registry: one C region per matched function.
13 0x80017AD4
14 0x8002D2A0
15 0x8002D2BC
16 0x80012780
+8 -1
View File
@@ -1,11 +1,18 @@
# Symbol registry: absolute addresses for cross-references used by C regions.
#
# Columns: NAME<TAB>address (address is hex; 0x prefix optional)
# Columns: NAME<TAB>address[<TAB>gp] (address is hex; 0x prefix optional)
#
# These name unmatched functions and globals so a C region can reference them
# by name. The harness passes each row to the linker as
# `--defsym NAME=0xADDR`. A name here is a placeholder for an address, not a
# claim about the object's meaning; real names are earned from evidence.
#
# A `gp` marker means the original accessed the symbol gp-relative, so the
# harness rewrites the macro access to explicit `%gp_rel`. `_gp` is the small
# data pointer value crt0 loads at 0x800FB3E4 (`lui gp,0x8012` +
# `addiu gp,gp,6456`), and is needed to resolve R_MIPS_GPREL16.
g_80122354 0x80122354
D_8012E2C8 0x8012E2C8
D_8010F354 0x8010F354
_gp 0x80121938
D_80121974 0x80121974 gp
1 # Symbol registry: absolute addresses for cross-references used by C regions.
2 #
3 # Columns: NAME<TAB>address (address is hex; 0x prefix optional) # Columns: NAME<TAB>address[<TAB>gp] (address is hex; 0x prefix optional)
4 #
5 # These name unmatched functions and globals so a C region can reference them
6 # by name. The harness passes each row to the linker as
7 # `--defsym NAME=0xADDR`. A name here is a placeholder for an address, not a
8 # claim about the object's meaning; real names are earned from evidence.
9 #
10 # A `gp` marker means the original accessed the symbol gp-relative, so the
11 # harness rewrites the macro access to explicit `%gp_rel`. `_gp` is the small
12 # data pointer value crt0 loads at 0x800FB3E4 (`lui gp,0x8012` +
13 # `addiu gp,gp,6456`), and is needed to resolve R_MIPS_GPREL16.
14 g_80122354
15 D_8012E2C8
16 D_8010F354
17 _gp
18 D_80121974
+5 -1
View File
@@ -88,7 +88,11 @@ g_80122354 0x80122354
`make code` and `make gate` pass `config/symbols.tsv` automatically; `range` accepts `--symbols`
(and `--defsym` for one-off names). A name in the registry is an address placeholder, not a claim
about the object's meaning. `0x8002D2A0` and `0x8002D2BC` are registered and gated this way.
about the object's meaning. `0x8002D2A0`, `0x8002D2BC` and `0x80012780` are registered and gated this way.
An optional third column `gp` marks a symbol the original accessed `gp`-relative (small data). The
harness rewrites that symbol's macro accesses to explicit `%gp_rel(sym)($gp)`, and the linker resolves
`R_MIPS_GPREL16` against the `_gp` row. See [PHASE6_SMALL_DATA.md](PHASE6_SMALL_DATA.md).
Symbols are resolved by the **linker**, not the assembler: the assembler leaves them undefined and
emits `%hi`/`%lo` relocations, and the linker applies the HI16 carry adjustment. This is what makes a
+20 -2
View File
@@ -112,9 +112,27 @@ this executable overwhelmingly `t0`.
*Limit:* the register choice is context-dependent, so it cannot be used to identify the compiler; it
only matters when reproducing a specific function.
### 10. `gp`-relative access is per symbol, forced with `%gp_rel`
- The `gp` base is **`0x80121938`**: crt0 does `lui gp,0x8012` + `addiu gp,gp,6456` at `0x800FB3E4`.
- The original accesses some globals `gp`-relative and others absolutely **even within ±32 KB of
`gp`**: `0x80122354` is absolute (`func_8002D2A0`) while `0x80121974` is `gp`-relative
(`func_80012780`). The choice is the object's **size/section**, not its distance from `gp`.
- GNU `as` only expands `lw`/`sw` macros to `%gp_rel` for an undefined symbol when `-G>0`, and it does
not do so for `sw` at all without the symbol's section. The harness therefore marks `gp` symbols in
the registry and rewrites their accesses to explicit `%gp_rel(sym)($gp)`; the linker resolves
`R_MIPS_GPREL16` against `_gp`.
*Basis:* `func_80012780` (`sw a0,60(gp)`) byte-identical; a census of 3,976 `gp`-relative accesses
over 1,286 distinct addresses. See [PHASE6_SMALL_DATA.md](PHASE6_SMALL_DATA.md).
*Limit:* the numeric `-G` threshold is **not recoverable** from the code (object sizes are unknown);
`-mgpopt`/`-mno-gpopt` produce identical `cc1` output for these functions.
## Open questions
- The exact `-G` small-data threshold (the executable clearly uses `gp`-relative data in places).
- Whether `-mgpopt` was passed.
- The exact `-G` small-data threshold is not recoverable from the code; the per-symbol `gp` form is
reconstructed from the original's accesses instead (finding 10).
- Whether `-mgpopt` was passed is not observable: it does not change `cc1` output for the cases
tested.
- One reconstructed function (`0x8005DEF8`) whose constant multiply the original emits as a real
`mult` while every compiler tested synthesises it — the reconstruction is the likely cause.
+101
View File
@@ -0,0 +1,101 @@
# Phase 6 — Small Data (`gp`) Evidence
**Scope:** P6-T4 — determine the small-data threshold from the executable's `gp`-relative accesses and
reproduce at least one `gp`-relative function byte-for-byte.
**Status:** complete. One `gp`-relative function matched; the numeric threshold is recorded as **not
uniquely recoverable** from the code, with the evidence and limits stated.
## The `gp` base
crt0 loads `gp` at `0x800FB3E4` from an absolute value:
| Address | Instruction | Value |
|---|---|---|
| `0x800FB3E4` | `lui gp,0x8012` | high half |
| `0x800FB3E8` | `addiu gp,gp,6456` | `gp = 0x80121938` |
So the small-data pointer for this executable is **`0x80121938`**.
## Access census
Scanning every load/store with an immediate base register of `gp`:
- **3,976** such instructions.
- **1,286** distinct target addresses, spanning `0x80119938..0x801298CF` (≈64 KB), with 257 gaps
larger than 64 bytes — i.e. many separate small objects.
- The most common offsets are 32–46 (addresses `0x80121958..0x80121966`), suggesting one or more
small objects close to `gp`.
- `gp`-relative **address** materialisation (`addiu rt,gp,imm`) also occurs, e.g. `addiu r4,gp,44`,
so the original also took the address of small objects.
## Why the numeric threshold is not recoverable here
GNU `as` behaviour (measured): with `-G N` where `N > 0`, the symbol macros `lw rt,sym` / `sw rt,sym`
for an **undefined** symbol expand to `%gp_rel`; with `-G0` they expand to `lui`/`%lo` absolute form.
But the original's choice is **per symbol**, not a single range test. The counter-example is decisive:
| Symbol | Address | Original access |
|---|---|---|
| `g_80122354` (`func_8002D2A0`) | `0x80122354` | **absolute** (`lui v0,0x8012` / `lw v0,9044(v0)`) |
| `D_80121974` (`func_80012780`) | `0x80121974` | **`gp`-relative** (`sw a0,60(gp)`) |
`0x80122354` is inside ±32 KB of `gp` (offset `+0xA1C`), yet is accessed absolutely. The difference is
the **size/section** of each object (`-G` puts objects ≤ N bytes in `.sdata`/`.sbss`), and object sizes
are not observable from the code because the data is the untouched payload fallback.
**Conclusion:** the bytes determine *which symbols* were accessed `gp`-relative, not the numeric `-G`
value. A lower bound is that the small-data region spans the full signed 16-bit offset range; the
largest `gp`-relative object's size would bound `-G` from above, but that size is unknown.
## `-mgpopt` evidence
`cc1` output is **identical** for `-G0`, `-G4`, `-G8`, `-G16`, `-G32`, and identical with `-mgpopt`
versus `-mno-gpopt`, for `extern int`/`char`/struct loads and stores:
```
cc1 -quiet -O2 -G8 -mno-split-addresses [-mgpopt|-mno-gpopt] -> identical .s
```
The `gp`-versus-absolute decision is therefore made by the assembler (from the symbol's section) and
the linker, not by `cc1`. `-mgpopt` is **not observable** from these functions and is left unselected.
## The reproduction
`func_80012780` (`0x80012780..0x8001278C`, 12 bytes):
```
sw a0,60(gp)
jr ra
nop
```
Mechanism (all now in the harness):
1. `config/symbols.tsv` marks `D_80121974` with the `gp` marker and defines `_gp = 0x80121938`.
2. The harness rewrites the maspsx output `sw $4,D_80121974` to
`sw $4,%gp_rel(D_80121974)($gp)` — GNU `as` will not do this itself for a symbol whose section it
cannot see.
3. GNU `as -G0` emits `R_MIPS_GPREL16`; the linker resolves it against `--defsym _gp=0x80121938`.
Result: `sf3_match range` → 0 differing bytes; `make gate` → `c_regions=4`, 0 differing bytes, SHA-1
`e173426c157384ebf1b6caf8c6fea18a85a14af9`.
Exact commands:
```bash
./tools/sf3_match range \
--exe 'extracted/SCUS_946.40;1' --source src/func_80012780.c \
--start 0x80012780 --end 0x8001278C --work .run/p6-t4-gp \
--symbols config/symbols.tsv
# -> result=MATCH
```
## Limits
- `gp` symbols are identified from the original's access form, not from a recovered symbol table; the
registry marker is the reconstruction of that per-symbol property.
- The `la`-of-a-`gp`-symbol rewrite (`addiu rt,$gp,%gp_rel(sym)`) is implemented but not yet exercised
on a real function.
- The numeric `-G` remains unresolved and is not needed by the current mechanism; it would matter only
if a region defined its own small data.
+36 -2
View File
@@ -9,8 +9,8 @@
- [x] **P6-T1 — Phase control records, baseline revalidation, and open-item triage** (complete)
- [x] **P6-T2 — Registry symbol support and per-region flag overrides** (complete)
- [x] **P6-T3 — `maspsx` integration and the ASPSX `la` verification** (complete)
- [ ] P6-T4 — `-G` small-data threshold from byte evidence
- [ ] Rules check
- [x] **P6-T4 — `-G` small-data threshold from byte evidence** (complete)
- [x] **Rules check** — re-read `AGENTS.md` mandatory behavior after P6-T4 and stated the required continuation notice.
- [ ] P6-T5 — Evidence-graded function-boundary inventory
- [ ] P6-T6 — First matching batch, with duplicate sharing
- [ ] P6-T7 — Cookbook, conventions, verification record, and phase gate
@@ -129,3 +129,37 @@ next code). This confirms the Phase 5 conclusion that the entry is CRT startup,
**Limits:** maspsx is exercised on `la`, symbol load/store and delay-slot scheduling; `-G`/gp-relative
behaviour is not yet exercised (P6-T4). The clear-loop negative is bounded to the tested reconstruction
and symbol names.
## P6-T4 — Small-data (`gp`) threshold from byte evidence (2026-09-23)
**Delivered:**
- `config/symbols.tsv` gained an optional `gp` marker and the `_gp` row (`0x80121938`, from crt0's
`lui gp,0x8012` + `addiu gp,gp,6456` at `0x800FB3E4`).
- The harness rewrites `gp`-marked symbols' macro accesses to explicit `%gp_rel(sym)($gp)`; the linker
resolves `R_MIPS_GPREL16` against `_gp`.
- Registered `0x80012780..0x8001278C` (`src/func_80012780.c`) — the first `gp`-relative match.
- Full evidence and limits in `docs/PHASE6_SMALL_DATA.md`.
**Findings:**
| Question | Byte-evidenced answer |
|---|---|
| `gp` base | `0x80121938` (crt0, `0x800FB3E4`) |
| How many `gp`-relative accesses | 3,976 over 1,286 distinct addresses, spanning ≈64 KB |
| Is the threshold recoverable? | **No** — `0x80122354` is absolute and `0x80121974` is `gp`-relative, both within ±32 KB of `gp`; the choice is the object's size/section, which is unobservable |
| Does `-mgpopt` matter? | Not observable: `-mgpopt`/`-mno-gpopt` (and `-G0..-G32`) give identical `cc1` output |
**Verification:**
| Check | Result |
|---|---|
| `range --symbols config/symbols.tsv` for `func_80012780` | 12 bytes, 0 differing, `result=MATCH` |
| `make gate` | `c_regions=4`, 0 differing, SHA-1 `e173426c…`, exit 0 |
| Synthetic suite | 71 tests pass (gp-marker parsing, unknown-marker rejection, end-to-end gp rewrite) |
**Limits:** `gp` symbols are identified from the original's access form, not a recovered symbol table;
the numeric `-G` is unresolved and unnecessary under this mechanism; the `la`-of-a-`gp`-symbol rewrite
is implemented but not yet exercised on a real function.
**Rules check — re-read complete. Continuing with P6-T5.**
+32
View File
@@ -0,0 +1,32 @@
/*
* func_80012780 — 12 bytes at 0x80012780..0x8001278C
*
* Byte-identical reconstruction of a leaf setter that stores its argument to a
* **gp-relative** global. This is the project's first match whose bytes depend
* on the small-data (`gp`) addressing form.
*
* The observed instructions are:
* sw a0,60(gp) D_80121974 = x
* jr ra
* nop
*
* `gp` is 0x80121938: crt0 loads it at 0x800FB3E4 (`lui gp,0x8012` +
* `addiu gp,gp,6456`). The target is therefore 0x80121974.
*
* GNU `as` only turns the `sw rt,sym` macro into `%gp_rel` when it can see the
* symbol defined in `.sdata`/`.sbss`. Here the data is the untouched payload
* fallback, so the harness rewrites the access to explicit `%gp_rel` for
* symbols marked `gp` in `config/symbols.tsv`; the linker resolves
* R_MIPS_GPREL16 against `_gp`.
*
* LIMITS: the function name and the global's name and type are hypotheses
* reconstructed from the disassembly; only the compiled bytes are evidence.
* The global's size is unknown, so this match does not by itself pin the
* original `-G` threshold (see docs/PHASE6_SMALL_DATA.md).
*/
extern int D_80121974;
void func_80012780(int x) {
D_80121974 = x;
}
+75 -7
View File
@@ -231,30 +231,48 @@ def parse_regions(text: str) -> list[Region]:
return regions
# A tracked symbol registry: `NAME<TAB>address` rows naming absolute addresses.
# A tracked symbol registry: `NAME<TAB>address[<TAB>gp]` rows naming absolute
# addresses. The optional `gp` marker forces a gp-relative access.
_SYMBOL_NAME = re.compile(r"[A-Za-z_.][A-Za-z0-9_.]*\Z")
def parse_symbols(text: str) -> list[str]:
"""Parse a symbol registry into assembler `--defsym NAME=0xADDR` arguments."""
@dataclass(frozen=True)
class SymbolTable:
defsyms: tuple[str, ...] = ()
gp_names: frozenset[str] = frozenset()
def parse_symbols(text: str) -> SymbolTable:
"""Parse a symbol registry into linker `--defsym` args and gp markers.
Columns are `NAME<TAB>address[<TAB>gp]`. A `gp` marker means the original
accessed the symbol `gp`-relative, so the harness rewrites its macro
accesses to explicit `%gp_rel` (GNU `as` will not do this for a symbol whose
section it cannot see).
"""
defsyms: list[str] = []
gp_names: set[str] = set()
seen: set[str] = set()
for number, raw in enumerate(text.splitlines(), 1):
line = raw.split("#", 1)[0].strip()
if not line:
continue
fields = line.split()
if len(fields) != 2:
raise ToolError(f"symbols line {number}: expected 'NAME address'")
if len(fields) not in (2, 3):
raise ToolError(f"symbols line {number}: expected 'NAME address [gp]'")
name = fields[0]
if not _SYMBOL_NAME.match(name):
raise ToolError(f"symbols line {number}: invalid symbol name {name!r}")
if name in seen:
raise ToolError(f"symbols line {number}: duplicate symbol {name!r}")
address = parse_hex_address(fields[1], number)
if len(fields) == 3:
if fields[2] != "gp":
raise ToolError(f"symbols line {number}: unknown marker {fields[2]!r}")
gp_names.add(name)
seen.add(name)
defsyms.append(f"{name}=0x{address:X}")
return defsyms
return SymbolTable(tuple(defsyms), frozenset(gp_names))
def validate_regions(regions: Sequence[Region], exe: PsxExe) -> None:
@@ -390,10 +408,49 @@ class Toolchain:
cc1_flags: Sequence[str]
as_flags: Sequence[str]
defsyms: Sequence[str]
gp_symbols: frozenset[str] = frozenset()
maspsx: Path | None = None
aspsx_version: str = DEFAULT_ASPSX_VERSION
# A symbol macro access that can be forced gp-relative.
_GP_ACCESS = re.compile(
r"^(?P<indent>\s*)(?P<op>lw|lh|lhu|lb|lbu|lwl|lwr|sw|sh|sb|swl|swr)"
r"\s+(?P<rt>\$[A-Za-z0-9]+),\s*(?P<sym>[A-Za-z_.][A-Za-z0-9_.]*)"
r"(?P<addend>[+-][0-9]+)?\s*$"
)
_GP_LA = re.compile(
r"^(?P<indent>\s*)la\s+(?P<rt>\$[A-Za-z0-9]+),\s*"
r"(?P<sym>[A-Za-z_.][A-Za-z0-9_.]*)(?P<addend>[+-][0-9]+)?\s*$"
)
def rewrite_gp_accesses(text: str, gp_symbols: frozenset[str]) -> str:
"""Force `%gp_rel` access for symbols the original read through `gp`."""
if not gp_symbols:
return text
lines: list[str] = []
for line in text.splitlines():
match = _GP_ACCESS.match(line)
if match and match.group("sym") in gp_symbols:
symbol = match.group("sym") + (match.group("addend") or "")
lines.append(
f"{match.group('indent')}{match.group('op')} "
f"{match.group('rt')},%gp_rel({symbol})($gp)"
)
continue
match = _GP_LA.match(line)
if match and match.group("sym") in gp_symbols:
symbol = match.group("sym") + (match.group("addend") or "")
lines.append(
f"{match.group('indent')}addiu {match.group('rt')},"
f"$gp,%gp_rel({symbol})"
)
continue
lines.append(line)
return "\n".join(lines) + ("\n" if text.endswith("\n") else "")
def compile_c(source: Path, out_object: Path, work: Path, tools: Toolchain) -> None:
"""Preprocess, compile, ASPSX-emulate and assemble one C source.
@@ -415,6 +472,13 @@ def compile_c(source: Path, out_object: Path, work: Path, tools: Toolchain) -> N
assembly, transformed,
)
assembly = transformed
if tools.gp_symbols:
rewritten = work / (out_object.stem + ".gp.s")
rewritten.write_text(
rewrite_gp_accesses(assembly.read_text(encoding="utf-8"), tools.gp_symbols),
encoding="utf-8",
)
assembly = rewritten
run([str(tools.assembler), *tools.as_flags, "-o", str(out_object), str(assembly)])
@@ -680,9 +744,12 @@ def resolve_toolchain(args: argparse.Namespace) -> Toolchain:
raise ToolError("no preprocessor found; pass --cpp")
cpp = Path(located)
defsyms = list(args.defsym)
gp_names: set[str] = set()
if args.symbols is not None:
symbols_path = require_file(args.symbols, "symbol registry")
defsyms = parse_symbols(symbols_path.read_text(encoding="utf-8")) + defsyms
table = parse_symbols(symbols_path.read_text(encoding="utf-8"))
defsyms = list(table.defsyms) + defsyms
gp_names |= table.gp_names
maspsx = None
if not args.no_maspsx:
maspsx = require_file(args.maspsx, "maspsx")
@@ -696,6 +763,7 @@ def resolve_toolchain(args: argparse.Namespace) -> Toolchain:
cc1_flags=args.cc1_flag or DEFAULT_CC1_FLAGS,
as_flags=args.as_flag or DEFAULT_AS_FLAGS,
defsyms=defsyms,
gp_symbols=frozenset(gp_names),
maspsx=maspsx,
aspsx_version=args.aspsx_version,
)
+37 -6
View File
@@ -199,10 +199,20 @@ class RegionOptionTests(unittest.TestCase):
class SymbolTests(unittest.TestCase):
def test_parses_names_comments_and_hex(self) -> None:
defsyms = sf3_match.parse_symbols(
table = sf3_match.parse_symbols(
"# comment\n\ng_80122354\t0x80122354\nfunc_1\t80123456\n"
)
self.assertEqual(defsyms, ["g_80122354=0x80122354", "func_1=0x80123456"])
self.assertEqual(list(table.defsyms),
["g_80122354=0x80122354", "func_1=0x80123456"])
self.assertEqual(table.gp_names, frozenset())
def test_parses_a_gp_marker(self) -> None:
table = sf3_match.parse_symbols("g\t0x80121974\tgp\nother\t0x80122354\n")
self.assertEqual(table.gp_names, frozenset({"g"}))
def test_rejects_an_unknown_marker(self) -> None:
with self.assertRaises(sf3_match.ToolError):
sf3_match.parse_symbols("g\t0x80121974\tbig\n")
def test_rejects_a_malformed_line(self) -> None:
with self.assertRaises(sf3_match.ToolError):
@@ -240,7 +250,7 @@ int synthetic_get(Synthetic *p) { return p->value; }
class EndToEndTests(unittest.TestCase):
"""Build a synthetic executable whose payload starts with real compiler output."""
def _toolchain(self, *, cc1_flags=(), as_flags=(), defsyms=()) -> object:
def _toolchain(self, *, cc1_flags=(), as_flags=(), defsyms=(), symbols=None) -> object:
args = type("Args", (), {})()
args.cpp = None
args.cc1 = sf3_match.DEFAULT_CC1
@@ -251,7 +261,7 @@ class EndToEndTests(unittest.TestCase):
args.cc1_flag = list(cc1_flags)
args.as_flag = list(as_flags)
args.defsym = list(defsyms)
args.symbols = None
args.symbols = symbols
args.maspsx = sf3_match.DEFAULT_MASPSX
args.no_maspsx = False
args.aspsx_version = sf3_match.DEFAULT_ASPSX_VERSION
@@ -259,13 +269,14 @@ class EndToEndTests(unittest.TestCase):
def _compile_payload(
self, root: Path, *, source_text: str = SOURCE, cc1_flags=(),
as_flags=(), defsyms=(), corrupt: bool = False,
as_flags=(), defsyms=(), symbols=None, corrupt: bool = False,
) -> tuple[Path, int]:
source = root / "synthetic.c"
source.write_text(source_text, encoding="ascii")
work = root / "work"
obj = root / "synthetic.o"
tools = self._toolchain(cc1_flags=cc1_flags, as_flags=as_flags, defsyms=defsyms)
tools = self._toolchain(cc1_flags=cc1_flags, as_flags=as_flags,
defsyms=defsyms, symbols=symbols)
sf3_match.compile_c(source, obj, work, tools)
code = sf3_match.link_object_bytes(obj, PAYLOAD, work, tools)
payload = bytearray(max(0x40, len(code) + 0x10))
@@ -354,6 +365,26 @@ class EndToEndTests(unittest.TestCase):
self.assertEqual(rc, 0)
self.assertEqual((out / "scus_946_40.rebuilt").read_bytes(), exe.read_bytes())
def test_gp_marker_forces_a_gp_relative_access(self) -> None:
source = "extern int g_gp;\nvoid synthetic_gp(int x) { g_gp = x; }\n"
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
symbols = self._symbols(root, "g_gp\t0x80121974\tgp\n_gp\t0x80121938\n")
exe, code_length = self._compile_payload(
root, source_text=source, symbols=symbols
)
regions = self._registry(root, code_length)
out = root / "out"
rc = sf3_match.main(["gate", "--exe", str(exe), "--regions", str(regions),
"--symbols", str(symbols), "--out", str(out)])
self.assertEqual(rc, 0)
self.assertEqual((out / "scus_946_40.rebuilt").read_bytes(), exe.read_bytes())
# Without the gp marker the access is absolute and the gate fails.
plain = self._symbols(root, "g_gp\t0x80121974\n_gp\t0x80121938\n")
rc = sf3_match.main(["gate", "--exe", str(exe), "--regions", str(regions),
"--symbols", str(plain), "--out", str(root / "out2")])
self.assertEqual(rc, 1)
def test_refuses_an_existing_destination(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)