1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
Aiden
62d1c3c876 C pseudo code 2026-05-25 14:17:56 +10:00
Aiden
4649cf530f Cycle states 2026-05-25 13:54:34 +10:00
9 changed files with 23737 additions and 2554 deletions

View File

@@ -9,7 +9,7 @@ python h8536_decompiler.py ROM\M27C512@DIP28_1.BIN --out build\rom_decompiled.as
If you are using the repo-local venv: If you are using the repo-local venv:
```powershell ```powershell
.\.venv\Scripts\python.exe h8536_decompiler.py --out build\rom_decompiled.asm --json build\rom_decompiled.json --callgraph-dot build\callgraph.dot .\.venv\Scripts\python.exe h8536_decompiler.py --out build\rom_decompiled.asm --json build\rom_decompiled.json --cycles --callgraph-dot build\callgraph.dot
``` ```
## What It Does ## What It Does
@@ -25,6 +25,7 @@ If you are using the repo-local venv:
- Parses the DTC vector table described by the manual. - Parses the DTC vector table described by the manual.
- Scans unreached ROM ranges for ASCII strings and pointer-table candidates. - Scans unreached ROM ranges for ASCII strings and pointer-table candidates.
- Emits function summaries and a direct-call graph in JSON, with optional Graphviz DOT output. - Emits function summaries and a direct-call graph in JSON, with optional Graphviz DOT output.
- Adds Appendix A cycle estimates to JSON and can append them to ASM comments.
- Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`. - Handles the E-clock transfer instructions `MOVFPE` and `MOVTPE`.
The generated listing is written to: The generated listing is written to:
@@ -50,6 +51,7 @@ python h8536_decompiler.py --help
- `--linear`: linear-sweep the selected range instead of tracing from vectors. - `--linear`: linear-sweep the selected range instead of tracing from vectors.
- `--start H'1000 --end H'D100`: constrain the decode range. - `--start H'1000 --end H'D100`: constrain the decode range.
- `--br H'FE`: resolve short absolute `@aa:8` operands through a known base-register value. - `--br H'FE`: resolve short absolute `@aa:8` operands through a known base-register value.
- `--cycles`: append Appendix A cycle estimates to assembly comments.
- `--callgraph-dot build\callgraph.dot`: write a Graphviz DOT call graph. - `--callgraph-dot build\callgraph.dot`: write a Graphviz DOT call graph.
## Code Layout ## Code Layout
@@ -62,5 +64,6 @@ python h8536_decompiler.py --help
- `h8536/analysis.py`: recursive tracing, linear sweep, labels, function grouping, and call graph analysis. - `h8536/analysis.py`: recursive tracing, linear sweep, labels, function grouping, and call graph analysis.
- `h8536/data_analysis.py`: unreached string and pointer-table candidate scans. - `h8536/data_analysis.py`: unreached string and pointer-table candidate scans.
- `h8536/memory.py`: manual-derived memory-region tagging. - `h8536/memory.py`: manual-derived memory-region tagging.
- `h8536/cycles.py`: Appendix A cycle estimate tables.
- `h8536/render.py`: assembly and JSON output. - `h8536/render.py`: assembly and JSON output.
- `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers. - `h8536/model.py`, `h8536/rom.py`, `h8536/formatting.py`: shared data structures and helpers.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3420
build/rom_pseudocode.c Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import argparse
from pathlib import Path from pathlib import Path
from .analysis import build_call_graph, collect_labels, linear_sweep, trace from .analysis import build_call_graph, collect_labels, linear_sweep, trace
from .cycles import annotate_cycles
from .data_analysis import analyze_unreached_data from .data_analysis import analyze_unreached_data
from .decoder import H8536Decoder from .decoder import H8536Decoder
from .formatting import parse_int from .formatting import parse_int
@@ -31,6 +32,7 @@ def main() -> int:
parser.add_argument("--entry", type=parse_int, action="append", default=[], help="extra entry point to trace") parser.add_argument("--entry", type=parse_int, action="append", default=[], help="extra entry point to trace")
parser.add_argument("--br", type=parse_int, default=None, help="optional BR value for @aa:8 short absolute operands") parser.add_argument("--br", type=parse_int, default=None, help="optional BR value for @aa:8 short absolute operands")
parser.add_argument("--linear", action="store_true", help="linear-sweep the selected range instead of tracing from vectors") parser.add_argument("--linear", action="store_true", help="linear-sweep the selected range instead of tracing from vectors")
parser.add_argument("--cycles", action="store_true", help="append Appendix A cycle estimates to assembly comments")
parser.add_argument("--callgraph-dot", type=Path, default=None, help="optional Graphviz DOT call graph output") parser.add_argument("--callgraph-dot", type=Path, default=None, help="optional Graphviz DOT call graph output")
args = parser.parse_args() args = parser.parse_args()
@@ -60,6 +62,7 @@ def main() -> int:
else: else:
instructions = trace(decoder, starts, args.start, end) instructions = trace(decoder, starts, args.start, end)
labels.update(collect_labels(instructions.values(), vectors)) labels.update(collect_labels(instructions.values(), vectors))
annotate_cycles(instructions, args.mode)
data_candidates = analyze_unreached_data(rom, instructions, args.start, end) data_candidates = analyze_unreached_data(rom, instructions, args.start, end)
call_graph = build_call_graph(instructions, vectors, labels) call_graph = build_call_graph(instructions, vectors, labels)
@@ -75,6 +78,7 @@ def main() -> int:
traced=not args.linear, traced=not args.linear,
dtc_vectors=dtc_vectors, dtc_vectors=dtc_vectors,
data_candidates=data_candidates, data_candidates=data_candidates,
show_cycles=args.cycles,
), ),
encoding="utf-8", encoding="utf-8",
) )

376
h8536/cycles.py Normal file
View File

@@ -0,0 +1,376 @@
from __future__ import annotations
from .decoder import is_ea_byte
from .model import Instruction
CYCLE_SOURCE = "manual Appendix A.4, tables A-7/A-8"
CYCLE_ASSUMPTION = "on-chip instruction fetch/operand access, no external wait states"
def annotate_cycles(instructions: dict[int, Instruction], mode: str) -> None:
for instruction in instructions.values():
instruction.cycles = estimate_cycles(instruction, mode)
def cycle_comment(cycles: dict[str, object] | None) -> str:
if not cycles:
return ""
if "cycles" in cycles:
return f"cycles={cycles['cycles']}"
if "cycles_min" in cycles and "cycles_max" in cycles:
return f"cycles={cycles['cycles_min']}-{cycles['cycles_max']}"
if "not_taken" in cycles and "taken" in cycles:
return f"cycles={cycles['not_taken']}/{cycles['taken']} nt/t"
if "trap_not_taken" in cycles:
return f"cycles={cycles['trap_not_taken']}/{cycles['trap_taken']} no-trap/trap"
if "false" in cycles:
return f"cycles={cycles['false']}/{cycles['count_minus_1']}/{cycles['taken']} false/-1/t"
return ""
def estimate_cycles(instruction: Instruction, mode: str) -> dict[str, object] | None:
if not instruction.valid:
return None
if instruction.raw and is_ea_byte(instruction.raw[0]):
base = _estimate_general_cycles(instruction, mode)
else:
base = _estimate_direct_cycles(instruction, mode)
if base is None:
return None
base.setdefault("source", CYCLE_SOURCE)
base.setdefault("assumption", CYCLE_ASSUMPTION)
return base
def _info(
cycles: int,
base_cycles: int | None = None,
adjustment: int = 0,
stack_adjustment: int = 0,
note: str | None = None,
) -> dict[str, object]:
info: dict[str, object] = {"cycles": cycles}
if base_cycles is not None:
info["base_cycles"] = base_cycles
if adjustment:
info["alignment_adjustment"] = adjustment
if stack_adjustment:
info["stack_adjustment"] = stack_adjustment
if note:
info["note"] = note
return info
def _branch_info(not_taken: int, taken: int, instruction: Instruction) -> dict[str, object]:
adjusted_taken = taken + _branch_adjust(instruction.address)
info: dict[str, object] = {
"not_taken": not_taken,
"taken": adjusted_taken,
"base_taken": taken,
}
if adjusted_taken != taken:
info["alignment_adjustment_taken"] = adjusted_taken - taken
return info
def _mode_min_max(mode: str, minimum: int, maximum: int) -> int:
return maximum if mode == "max" else minimum
def _branch_adjust(address: int) -> int:
# Table A-8(a): branch/system transfer adjustment is 0 at even starts, 1 at odd starts.
return address & 1
def _ea_mode(raw: bytes) -> tuple[str, str] | None:
if not raw:
return None
b = raw[0]
if 0xA0 <= b <= 0xAF:
return "reg", "W" if b & 0x08 else "B"
if 0xB0 <= b <= 0xBF:
return "predec", "W" if b & 0x08 else "B"
if 0xC0 <= b <= 0xCF:
return "postinc", "W" if b & 0x08 else "B"
if 0xD0 <= b <= 0xDF:
return "indirect", "W" if b & 0x08 else "B"
if 0xE0 <= b <= 0xEF:
return "disp8", "W" if b & 0x08 else "B"
if 0xF0 <= b <= 0xFF:
return "disp16", "W" if b & 0x08 else "B"
if b == 0x04:
return "imm", "B"
if b == 0x0C:
return "imm", "W"
if b == 0x05:
return "abs8", "B"
if b == 0x0D:
return "abs8", "W"
if b == 0x15:
return "abs16", "B"
if b == 0x1D:
return "abs16", "W"
return None
def _a8_adjust(address: int, ea_mode: str, size: str, mov_imm_to_ea: bool = False) -> int:
# Table A-8(b), simplified for decoded modes used by this tool.
if mov_imm_to_ea:
if size == "B":
return 1
even = {
"reg": 2,
"predec": 0,
"postinc": 2,
"indirect": 2,
"disp8": 2,
"disp16": 0,
"abs8": 2,
"abs16": 2,
}
odd = {
"reg": 0,
"predec": 2,
"postinc": 0,
"indirect": 0,
"disp8": 0,
"disp16": 2,
"abs8": 0,
"abs16": 0,
}
return (odd if address & 1 else even).get(ea_mode, 0)
even_default = {
"reg": 0,
"predec": 1,
"postinc": 0,
"indirect": 1,
"disp8": 1,
"disp16": 1,
"abs8": 0,
"abs16": 1,
"imm": 0,
}
odd_default = {
"reg": 0,
"predec": 0,
"postinc": 1,
"indirect": 0,
"disp8": 0,
"disp16": 0,
"abs8": 1,
"abs16": 0,
"imm": 0,
}
return (odd_default if address & 1 else even_default).get(ea_mode, 0)
def _memory_read_base(ea_mode: str, size: str) -> int:
if ea_mode == "reg":
return 2 if size == "B" else 3
if ea_mode == "imm":
return 3 if size == "B" else 4
if ea_mode in {"disp8", "disp16", "abs16"}:
return 6
return 5
def _memory_write_base(ea_mode: str, size: str) -> int:
if ea_mode == "reg":
return 2 if size == "B" else 3
if ea_mode in {"disp8", "disp16", "abs16"}:
return 6
return 5
def _read_modify_write_base(ea_mode: str, size: str) -> int:
if ea_mode == "reg":
return 2 if size == "B" else 3
if ea_mode in {"disp8", "disp16", "abs16"}:
return 8
return 7
def _imm_to_ea_base(ea_mode: str, size: str) -> int:
if ea_mode == "reg":
return 2 if size == "B" else 3
if ea_mode in {"disp8", "disp16", "abs16"}:
return 8 if size == "B" else 9
return 7 if size == "B" else 8
def _estimate_general_cycles(instruction: Instruction, mode: str) -> dict[str, object] | None:
parsed = _ea_mode(instruction.raw)
if parsed is None or len(instruction.raw) < 2:
return None
ea_mode, ea_size = parsed
mnemonic = instruction.mnemonic
op = instruction.raw[1 + (1 if ea_mode in {"imm", "abs8"} else 2 if ea_mode == "abs16" else 0)]
if ea_mode in {"disp8"}:
op = instruction.raw[2]
elif ea_mode == "disp16":
op = instruction.raw[3]
mov_imm_to_ea = mnemonic.startswith("MOV:G") and instruction.operands.startswith("#")
adjustment = _a8_adjust(instruction.address, ea_mode, ea_size, mov_imm_to_ea)
if mnemonic.startswith(("ADD:G", "ADDS", "SUB", "SUBS", "ADDX", "SUBX", "AND.", "OR.", "XOR.", "CMP:G")):
base = _memory_read_base(ea_mode, ea_size)
elif mnemonic.startswith("MOV:G"):
base = _imm_to_ea_base(ea_mode, ea_size) if mov_imm_to_ea else _memory_write_base(ea_mode, ea_size)
elif mnemonic.startswith(("BTST", "TST")):
base = _memory_read_base(ea_mode, ea_size)
elif mnemonic.startswith(("BSET", "BCLR", "BNOT", "CLR", "NEG", "NOT", "SHAL", "SHAR", "SHLL", "SHLR", "ROTL", "ROTR", "ROTXL", "ROTXR", "TAS")):
base = _read_modify_write_base(ea_mode, ea_size)
elif mnemonic.startswith("ADD:Q"):
base = 4 if ea_mode == "reg" else _read_modify_write_base(ea_mode, ea_size)
elif mnemonic.startswith("MULXU.B"):
base = 18 if ea_mode == "reg" else 19
elif mnemonic.startswith("MULXU.W"):
base = 25
elif mnemonic.startswith("DIVXU.B"):
base = 21 if ea_mode == "reg" else 23
elif mnemonic.startswith("DIVXU.W"):
base = 28 if ea_mode == "reg" else 29
elif mnemonic.startswith(("LDC.B", "STC.B")):
base = 4 if ea_mode in {"reg", "imm"} else 6 if ea_mode in {"abs8", "indirect", "predec", "postinc"} else 7
elif mnemonic.startswith(("LDC.W", "STC.W")):
base = 6 if ea_mode in {"reg", "imm"} else 7 if ea_mode in {"abs8", "indirect", "predec", "postinc"} else 8
elif mnemonic.startswith(("ORC", "ANDC", "XORC")):
base = 4
adjustment = 0
elif mnemonic.startswith(("EXTS", "EXTU", "SWAP")):
base = 3
adjustment = 0
elif mnemonic.startswith(("MOVFPE", "MOVTPE")):
cycles = _mode_min_max(mode, 13, 20)
return _info(cycles, note="E-clock peripheral transfer")
else:
return None
return _info(base + adjustment, base, adjustment)
def _estimate_direct_cycles(instruction: Instruction, mode: str) -> dict[str, object] | None:
raw = instruction.raw
mnemonic = instruction.mnemonic
if not raw:
return None
op = raw[0]
if mnemonic.startswith("B") and 0x20 <= op <= 0x3F:
info = _branch_info(3, 7, instruction)
if mnemonic == "BRA":
info["cycles"] = info["taken"]
elif mnemonic == "BRN":
info["cycles"] = info["not_taken"]
return info
if mnemonic.startswith("SCB/"):
taken = 8 + _branch_adjust(instruction.address)
return {
"false": 3,
"count_minus_1": 4,
"taken": taken,
"base_taken": 8,
}
if mnemonic == "BSR":
base = 9
adjust = _branch_adjust(instruction.address)
stack = 4
return _info(base + stack + adjust, base, adjust, stack, "PC word push to stack")
if mnemonic == "JMP":
if op == 0x10:
base = 7
elif len(raw) >= 2 and raw[0] == 0x11 and 0xD0 <= raw[1] <= 0xD7:
base = 6
elif len(raw) >= 2 and raw[0] == 0x11 and 0xE0 <= raw[1] <= 0xE7:
base = 7
elif len(raw) >= 2 and raw[0] == 0x11 and 0xF0 <= raw[1] <= 0xF7:
base = 8
else:
return None
adjust = _branch_adjust(instruction.address)
return _info(base + adjust, base, adjust)
if mnemonic == "JSR":
if op == 0x18:
base = 9
elif len(raw) >= 2 and raw[0] == 0x11 and 0xD8 <= raw[1] <= 0xDF:
base = 9
elif len(raw) >= 2 and raw[0] == 0x11 and 0xE8 <= raw[1] <= 0xEF:
base = 9
elif len(raw) >= 2 and raw[0] == 0x11 and 0xF8 <= raw[1] <= 0xFF:
base = 10
else:
return None
adjust = _branch_adjust(instruction.address)
stack = 4
return _info(base + stack + adjust, base, adjust, stack, "PC word push to stack")
if mnemonic == "PJMP":
base = 9 if op == 0x13 else 8
adjust = _branch_adjust(instruction.address)
return _info(base + adjust, base, adjust)
if mnemonic == "PJSR":
base = 15 if op == 0x03 else 13
adjust = _branch_adjust(instruction.address)
stack = 6 if mode == "max" else 4
return _info(base + stack + adjust, base, adjust, stack, "return address push to stack")
if mnemonic == "TRAPA":
return _info(_mode_min_max(mode, 17, 22))
if mnemonic == "TRAP/VS":
return {
"trap_not_taken": 3,
"trap_taken": _mode_min_max(mode, 18, 23) + _branch_adjust(instruction.address),
"base_trap_taken": _mode_min_max(mode, 18, 23),
}
if mnemonic == "RTE":
base = _mode_min_max(mode, 13, 15)
adjust = _branch_adjust(instruction.address)
return _info(base + adjust, base, adjust)
if mnemonic == "RTS":
base = 8
adjust = _branch_adjust(instruction.address)
stack = 4
return _info(base + stack + adjust, base, adjust, stack, "PC word pop from stack")
if mnemonic == "RTD":
base = 9
adjust = _branch_adjust(instruction.address)
stack = 4
return _info(base + stack + adjust, base, adjust, stack, "PC word pop from stack")
if mnemonic == "PRTS":
base = 12
adjust = _branch_adjust(instruction.address)
stack = 6 if mode == "max" else 4
return _info(base + stack + adjust, base, adjust, stack, "return address pop from stack")
if mnemonic == "PRTD":
base = 13
adjust = _branch_adjust(instruction.address)
stack = 6 if mode == "max" else 4
return _info(base + stack + adjust, base, adjust, stack, "return address pop from stack")
if mnemonic == "UNLK":
return _info(5)
if mnemonic == "LINK":
return _info(6 if len(raw) == 2 else 7)
if mnemonic == "LDM.W":
n = raw[1].bit_count()
cycles = 6 + 4 * n
return _info(cycles, note=f"6+4n, n={n}")
if mnemonic == "STM.W":
n = raw[1].bit_count()
cycles = 6 + 3 * n
return _info(cycles, note=f"6+3n, n={n}")
if mnemonic in {"NOP", "SLEEP"}:
return _info(2)
if mnemonic.startswith("CMP:E"):
return _info(2)
if mnemonic.startswith("CMP:I"):
return _info(3)
if mnemonic.startswith("MOV:E"):
return _info(2)
if mnemonic.startswith("MOV:I"):
return _info(3)
if mnemonic.startswith(("MOV:L", "MOV:S", "MOV:F")):
return _info(5)
return None

View File

@@ -28,6 +28,7 @@ class Instruction:
references: list[int] = field(default_factory=list) references: list[int] = field(default_factory=list)
writes_br: bool = False writes_br: bool = False
br_value: int | None = None br_value: int | None = None
cycles: dict[str, object] | None = None
@property @property
def size(self) -> int: def size(self) -> int:

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import json import json
from pathlib import Path from pathlib import Path
from .cycles import cycle_comment
from .formatting import h16, label_for from .formatting import h16, label_for
from .memory import MEMORY_REGIONS, region_for from .memory import MEMORY_REGIONS, region_for
from .model import Instruction from .model import Instruction
@@ -30,6 +31,7 @@ def format_listing(
traced: bool, traced: bool,
dtc_vectors: dict[int, DtcVectorEntry] | None = None, dtc_vectors: dict[int, DtcVectorEntry] | None = None,
data_candidates: dict[str, list[dict[str, object]]] | None = None, data_candidates: dict[str, list[dict[str, object]]] | None = None,
show_cycles: bool = False,
) -> str: ) -> str:
lines: list[str] = [] lines: list[str] = []
lines.append("; H8/536 ROM disassembly") lines.append("; H8/536 ROM disassembly")
@@ -43,6 +45,8 @@ def format_listing(
lines.append("; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.") lines.append("; - In minimum mode the reset vector at H'0000-H'0001 is a 16-bit PC.")
lines.append("; - The register field is H'FE80-H'FFFF; names below come from appendix B.") lines.append("; - The register field is H'FE80-H'FFFF; names below come from appendix B.")
lines.append("; - @aa:8 short absolute operands use BR as the upper address byte.") lines.append("; - @aa:8 short absolute operands use BR as the upper address byte.")
if show_cycles:
lines.append("; - Cycle counts use Appendix A tables A-7/A-8 for on-chip access with no external wait states.")
lines.append("") lines.append("")
lines.append("; Memory Map") lines.append("; Memory Map")
for region in MEMORY_REGIONS: for region in MEMORY_REGIONS:
@@ -84,7 +88,15 @@ def format_listing(
lines.append(f"{labels[address]}:") lines.append(f"{labels[address]}:")
raw = " ".join(f"{byte:02X}" for byte in ins.raw) raw = " ".join(f"{byte:02X}" for byte in ins.raw)
padded_raw = raw.ljust(14) padded_raw = raw.ljust(14)
comment_parts = [part for part in (ins.comment, _reference_comment(ins) if not ins.comment else "") if part] comment_parts = [
part
for part in (
ins.comment,
_reference_comment(ins) if not ins.comment else "",
cycle_comment(ins.cycles) if show_cycles else "",
)
if part
]
comment = f" ; {'; '.join(comment_parts)}" if comment_parts else "" comment = f" ; {'; '.join(comment_parts)}" if comment_parts else ""
lines.append(f"{address:04X}: {padded_raw} {ins.text}{comment}") lines.append(f"{address:04X}: {padded_raw} {ins.text}{comment}")
lines.append("") lines.append("")
@@ -128,6 +140,7 @@ def write_json(
"operands": ins.operands, "operands": ins.operands,
"kind": ins.kind, "kind": ins.kind,
"targets": ins.targets, "targets": ins.targets,
"cycles": ins.cycles,
"references": [ "references": [
{ {
"address": address, "address": address,

View File

@@ -0,0 +1,48 @@
import unittest
from h8536.cycles import estimate_cycles
from h8536.decoder import H8536Decoder
from h8536.rom import Rom
def decode_at(data: list[int], address: int = 0):
return H8536Decoder(Rom(bytes(data), base=address)).decode(address)
class ManualCycleExamplesTest(unittest.TestCase):
def test_add_word_register_indirect_matches_manual_even_and_odd_examples(self):
even = decode_at([0xD8, 0x21], 0x0100)
odd = decode_at([0xD8, 0x21], 0x0101)
self.assertEqual(even.text, "ADD:G.W @R0, R1")
self.assertEqual(estimate_cycles(even, "min")["cycles"], 6)
self.assertEqual(estimate_cycles(even, "min")["base_cycles"], 5)
self.assertEqual(estimate_cycles(even, "min")["alignment_adjustment"], 1)
self.assertEqual(odd.text, "ADD:G.W @R0, R1")
self.assertEqual(estimate_cycles(odd, "min")["cycles"], 5)
self.assertEqual(estimate_cycles(odd, "min")["base_cycles"], 5)
def test_jsr_register_indirect_matches_manual_stack_adjusted_example(self):
even = decode_at([0x11, 0xD8], 0xFC00)
odd = decode_at([0x11, 0xD8], 0xFC01)
self.assertEqual(even.text, "JSR @R0")
self.assertEqual(estimate_cycles(even, "min")["cycles"], 13)
self.assertEqual(estimate_cycles(even, "min")["base_cycles"], 9)
self.assertEqual(estimate_cycles(even, "min")["stack_adjustment"], 4)
self.assertEqual(odd.text, "JSR @R0")
self.assertEqual(estimate_cycles(odd, "min")["cycles"], 14)
self.assertEqual(estimate_cycles(odd, "min")["alignment_adjustment"], 1)
def test_conditional_branch_keeps_taken_and_not_taken_counts(self):
instruction = decode_at([0x26, 0x02], 0x0000)
cycles = estimate_cycles(instruction, "min")
self.assertEqual(cycles["not_taken"], 3)
self.assertEqual(cycles["taken"], 7)
if __name__ == "__main__":
unittest.main()