Add boxsim CLI: pure Python codec, netlist extraction, KiCad project generation
Replaces the bpj2ini.py prototype with a packaged tool. The A2F codec is now reimplemented in Python, so neither Boxsim.exe nor a CPU emulator is needed to decode a container; the emulator survives as a development-only oracle that the codec is differentially tested against. Verified: - 134 containers decode byte-identically to Boxsim's own decoder, 270 pre-2.0 files rejected by both implementations (tools/difftest.py --self-test) - Default.BPJ: 22 components, 14 nets, no dangling terminal - 22 of 22 parts resolve to library symbols; one series inferred from a sibling, two DC resistance deviations reported - ERC clean and the netlist KiCad extracts from the generated sheet is an identical partition to the one derived from the Boxsim grid - container build byte-identical to the host build - 32 tests passing, ruff clean Decode time went 69 s -> 14.5 s (binding emulator hooks to trap addresses) -> 1.6 s (pure Python). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb81ca61cb
commit
4737a5bae8
28 files changed
+4459
-122
No files matched your search
@@ -0,0 +1,94 @@
|
||||
"""Parser for the INI-ish text Boxsim stores inside its containers.
|
||||
|
||||
Shape: `#Section` headers, `key=<TAB>value` pairs, CRLF line ends, German
|
||||
decimal commas, and arrays as `;`-separated values. Section names are
|
||||
case sensitive: a project contains both `#AktFilter1` and `#Aktfilter1`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
class Section(dict):
|
||||
"""One `#Name` block. A plain dict of str -> str, plus the section name."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debugging aid
|
||||
return f"Section({self.name!r}, {len(self)} keys)"
|
||||
|
||||
|
||||
class IniDocument:
|
||||
"""Ordered collection of sections, tolerant of repeated section names."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sections: list[Section] = []
|
||||
|
||||
def __iter__(self) -> Iterator[Section]:
|
||||
return iter(self.sections)
|
||||
|
||||
def __contains__(self, name: str) -> bool:
|
||||
return any(s.name == name for s in self.sections)
|
||||
|
||||
def first(self, name: str) -> Section | None:
|
||||
for s in self.sections:
|
||||
if s.name == name:
|
||||
return s
|
||||
return None
|
||||
|
||||
def require(self, name: str) -> Section:
|
||||
s = self.first(name)
|
||||
if s is None:
|
||||
raise KeyError(f"missing section #{name}")
|
||||
return s
|
||||
|
||||
def all(self, name: str) -> list[Section]:
|
||||
return [s for s in self.sections if s.name == name]
|
||||
|
||||
def merged(self, *names: str) -> Section:
|
||||
"""Merge several sections into one, for split blocks like AktFilter."""
|
||||
out = Section(names[0])
|
||||
for name in names:
|
||||
for s in self.all(name):
|
||||
out.update(s)
|
||||
return out
|
||||
|
||||
|
||||
def parse(text: str) -> IniDocument:
|
||||
doc = IniDocument()
|
||||
current: Section | None = None
|
||||
for raw_line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
||||
line = raw_line.rstrip("\n")
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
current = Section(line[1:].strip())
|
||||
doc.sections.append(current)
|
||||
continue
|
||||
if current is None:
|
||||
# Content before any header: keep it in an unnamed section rather
|
||||
# than dropping it silently.
|
||||
current = Section("")
|
||||
doc.sections.append(current)
|
||||
key, sep, value = line.partition("=")
|
||||
if not sep:
|
||||
continue
|
||||
current[key.strip()] = value.strip()
|
||||
return doc
|
||||
|
||||
|
||||
def number(value: str) -> float:
|
||||
"""Parse a Boxsim number. German decimal comma, exponents like `1,E-5`."""
|
||||
return float(value.strip().replace(",", "."))
|
||||
|
||||
|
||||
def integer(value: str) -> int:
|
||||
return int(float(value.strip().replace(",", ".")))
|
||||
|
||||
|
||||
def number_list(value: str) -> list[float]:
|
||||
"""Parse a `;`-separated array, tolerating trailing separators."""
|
||||
return [number(p) for p in value.split(";") if p.strip()]
|
||||
Reference in new issue
Block a user