summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py
diff options
context:
space:
mode:
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py')
-rw-r--r--debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py1247
1 files changed, 1247 insertions, 0 deletions
diff --git a/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py b/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py
new file mode 100644
index 0000000..b47a028
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/harness/sim_qemu_line.py
@@ -0,0 +1,1247 @@
+#!/usr/bin/env python3
+"""S4 evidence harness: is the native GSP simulator above the QEMU line?
+
+Runs one A/B pair on the real TU104 570.144 GSP image:
+
+* **QEMU baseline** - the v0.1/v0.2 probe path (flat relocated load, bounded
+ wall-clock run, ``guest_errors,unimp,int`` log): first exception, address and
+ ladder rung, recorded as a normal ``greeneye-session/1``.
+* **Native baseline** - ``sim.gsp_profile`` (deterministic RV64IMAC, modeled
+ cycles): instructions, cycles, first modeled MMIO, stop reason and rung.
+ If S1's device model/profile is present (``sim/profiles/``, ``sim/devices/``,
+ or a Python ``sim.devices`` entry point) it is discovered defensively and
+ used as the MMIO map; otherwise the run is the lenient B0 baseline.
+
+The rung ladder is docs/35-GREENEYE-SIMULATOR-DESIGN.md section 3.3
+(``pre-B0`` < B0 < B1 < ... < B5). "Above the QEMU line" means the native
+rung ordinal is at least one past QEMU's, twice identically, within the frozen
+wall-clock threshold, and with a discovered S1 device model (docs/35 1.1);
+requiring the device model means a lenient run reports ``above_lenient_only``,
+not ``above``. The frozen record and the first measured results live in
+``docs/37-SIM-QEMU-LINE.md``.
+
+The harness also exercises the native feature matrix (hooks/breakpoints/
+watchpoints, memory/struct, sessions, compare, trace, injection) on the bundled
+demo firmware. MCP is deliberately ``not_run``: the native MCP surface is S3's
+deliverable (see docs/37 section "Not verified").
+
+Everything is stdlib-only, bounded (``<= 2M`` instructions per native run,
+``<= 10 s`` of QEMU) and touches no hardware, GPU, guest, module or network.
+
+CLI::
+
+ python3 harness/sim_qemu_line.py
+ python3 harness/sim_qemu_line.py --no-qemu --instructions 50000
+ python3 harness/sim_qemu_line.py --device-map sim/profiles/tu104-570.144.json
+"""
+from __future__ import annotations
+
+import argparse
+import hashlib
+import json
+import os
+import shutil
+import subprocess
+import sys
+import time
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+TOOL_ROOT = os.path.dirname(HERE)
+if TOOL_ROOT not in sys.path:
+ sys.path.insert(0, TOOL_ROOT)
+
+from greeneye.analyze import compare_sessions, parse_qemu_faults # noqa: E402
+from greeneye.device_map import ( # noqa: E402
+ QEMU_REFERENCE,
+ DeviceEntry,
+ DeviceMap,
+ firmware_sha256,
+ harvest_from_text,
+)
+from greeneye.errors import GreenEyeError # noqa: E402
+from harness.common import ( # noqa: E402
+ load_target,
+ open_flat_session,
+ qemu_available,
+ read_log_window,
+)
+from sim.gsp_profile import DEFAULT_IMAGE, run_profile # noqa: E402
+
+SCHEMA = "greeneye-sim-qemu-line/1"
+
+#: Boot-ladder rungs (docs/35 section 3.3), lowest first. ``pre-B0`` is the
+#: QEMU fault position: image loaded, first MMIO observed *as a fault*.
+RUNG_ORDER = ("pre-B0", "B0", "B1", "B2", "B3", "B4", "B5")
+RUNG_INDEX = {name: index for index, name in enumerate(RUNG_ORDER)}
+QEMU_RUNG = "pre-B0"
+NATIVE_LENIENT_RUNG = "B0"
+
+#: Default frozen thresholds (recorded 2026-10-03, before the first
+#: measurement). The measured run and rationale live in
+#: docs/37-SIM-QEMU-LINE.md section 3; the CLI uses these values when no
+#: override is passed. Never move silently.
+FROZEN_THRESHOLDS = {
+ "frozen_on": "2026-10-03",
+ "native_rungs_above_qemu": 1,
+ "identical_runs": 2,
+ "native_pair_wall_s": 20.0,
+ "qemu_first_fault_fw": QEMU_REFERENCE["firmware_vaddr"],
+ "qemu_first_touch": QEMU_REFERENCE["access_addr"],
+ # docs/35 1.1 requires the milestone to be reached "with the device model
+ # and shims documented": lenient MMIO is observation, not a device model,
+ # so it cannot satisfy the line contract on its own.
+ "device_model_required": True,
+}
+
+#: Bounds (eng-ops / task discipline).
+MAX_INSTRUCTIONS = 2_000_000
+MAX_QEMU_DURATION_S = 10.0
+DEFAULT_INSTRUCTIONS = 200_000
+DEFAULT_QEMU_DURATION_S = 0.5
+
+#: Default report directory (new, S4-owned).
+DEFAULT_OUT = os.path.join(HERE, "qemu_line")
+
+DEMO_ELF = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "build", "uproc_demo.elf")
+DEMO_SCHEMA = os.path.join(TOOL_ROOT, "firmware", "uproc_demo", "gsp_ctrl.schema.json")
+SDK_ELF = os.path.join(TOOL_ROOT, "sdk", "build", "injected_demo.elf")
+
+#: S1 device-model discovery: JSON conventions first, then a duck-typed Python
+#: API. See docs/37 section "Re-running when S1 lands".
+S1_JSON_DIRS = ("sim/profiles", "sim/devices")
+S1_PY_API_NAMES = (
+ "device_map_for_firmware",
+ "profile_for_firmware",
+ "load_device_map",
+ "load_profile",
+ "profile_for",
+)
+_RUNG_KEYS = ("rung", "highest_rung", "boot_ladder_rung", "milestone")
+
+
+class HarnessError(RuntimeError):
+ """A harness precondition failed in a way the caller should report."""
+
+
+# -- small helpers ---------------------------------------------------------
+
+
+def _relpath(path: str | None) -> str | None:
+ if path is None:
+ return None
+ return os.path.relpath(os.path.abspath(path), TOOL_ROOT)
+
+
+def _rm_tree(path: str) -> None:
+ shutil.rmtree(path, ignore_errors=True)
+
+
+def _write_text(path: str, text: str) -> str:
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ with open(path, "w", encoding="utf-8") as fh:
+ fh.write(text)
+ return path
+
+
+def _write_json(path: str, payload: dict) -> str:
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ tmp = f"{path}.tmp-{os.getpid()}"
+ with open(tmp, "w", encoding="utf-8") as fh:
+ json.dump(payload, fh, indent=2, sort_keys=True)
+ fh.write("\n")
+ os.replace(tmp, path)
+ return path
+
+
+def session_hashes(session_dir: str | None) -> dict:
+ """sha256 of a session's core evidence files ({} when the dir is absent)."""
+ if not session_dir or not os.path.isdir(session_dir):
+ return {}
+ hashes: dict = {}
+ for name in ("session.json", "events.jsonl", "summary.md"):
+ path = os.path.join(session_dir, name)
+ if not os.path.isfile(path):
+ continue
+ digest = hashlib.sha256()
+ with open(path, "rb") as fh:
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
+ digest.update(chunk)
+ hashes[name] = digest.hexdigest()
+ return hashes
+
+
+def _x86_vms_running() -> int | None:
+ """Count other agents' x86 QEMU VMs (never disturbed; recorded only)."""
+ try:
+ proc = subprocess.run(
+ ["pgrep", "-f", "qemu-system-x86_64"],
+ capture_output=True,
+ text=True,
+ timeout=5.0,
+ )
+ except (OSError, subprocess.SubprocessError):
+ return None
+ if proc.returncode not in (0, 1):
+ return None
+ pids = [line for line in proc.stdout.splitlines() if line.strip().isdigit()]
+ return len(pids)
+
+
+def rung_ordinal(rung: str | None) -> int | None:
+ if rung in RUNG_INDEX:
+ return RUNG_INDEX[rung]
+ return None
+
+
+def above_qemu_line(qemu_rung: str | None, native_rung: str | None, minimum: int = 1) -> bool:
+ """True when ``native_rung`` is at least ``minimum`` rungs past QEMU's."""
+ q = rung_ordinal(qemu_rung)
+ n = rung_ordinal(native_rung)
+ if q is None or n is None:
+ return False
+ return n >= q + minimum
+
+
+def _summary_fault(fault: dict, bias: int) -> dict:
+ access = fault.get("access") or {}
+ epc = int(fault.get("epc", 0) or 0)
+ return {
+ "desc": fault.get("desc"),
+ "cause": fault.get("cause"),
+ "epc": epc,
+ "epc_fw": (epc - bias) if (bias and epc and epc >= bias) else epc,
+ "access_addr": access.get("access_addr"),
+ "size": access.get("size"),
+ "rw": access.get("rw"),
+ "reason": access.get("reason"),
+ }
+
+
+# -- QEMU baseline ---------------------------------------------------------
+
+
+def run_qemu_baseline(
+ image: str = DEFAULT_IMAGE,
+ out_dir: str = DEFAULT_OUT,
+ duration: float = DEFAULT_QEMU_DURATION_S,
+) -> dict:
+ """Probe the real image under QEMU; skip cleanly when unavailable/absent."""
+ result: dict = {
+ "backend": "qemu",
+ "ran": False,
+ "reason": None,
+ "rung": None,
+ "session": None,
+ "wall_s": 0.0,
+ }
+ if duration <= 0 or duration > MAX_QEMU_DURATION_S:
+ raise HarnessError(f"qemu duration must be in (0, {MAX_QEMU_DURATION_S}], got {duration}")
+ if not os.path.isfile(image):
+ result["reason"] = f"firmware absent: {image}"
+ return result
+ if not qemu_available():
+ result["reason"] = "qemu-system-riscv64 not installed"
+ return result
+
+ start = time.monotonic()
+ image_obj = load_target(image)
+ session_dir = os.path.join(out_dir, "sessions", "qemu-probe")
+ _rm_tree(session_dir)
+ with open_flat_session(image_obj, image, record=session_dir, name="s4-qemu-probe") as flat:
+ flat.session.run(duration=duration)
+ pc = flat.session.pc
+ bias = flat.bias
+ # Read the log window once: `-d int` trap loops flood the file, so the
+ # first exception has to be parsed from the bounded head window.
+ log_text, capped = read_log_window(flat.target.qemu_log)
+ faults = parse_qemu_faults(log_text)
+ first = faults[0] if faults else None
+ device_map = harvest_from_text(log_text, bias=bias)
+ meta = {
+ "s4_qemu_rung": QEMU_RUNG,
+ "qemu_fault_count_window": len(faults),
+ "qemu_log_window_capped": capped,
+ "qemu_first_fault": first,
+ }
+ recorder = getattr(flat.session, "recorder", None)
+ if recorder is not None and hasattr(recorder, "meta"):
+ recorder.meta.update(meta)
+ if log_text and hasattr(recorder, "blob"):
+ recorder.meta["qemu_log_blob"] = recorder.blob(
+ "qemu.log", log_text.encode("utf-8")
+ )
+ result.update(
+ {
+ "ran": True,
+ "reason": "ok",
+ "rung": QEMU_RUNG,
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "stopped_pc": pc,
+ "fault_count_window": len(faults),
+ "log_window_capped": capped,
+ "first_fault": _summary_fault(first, bias) if first else None,
+ "first_touch_match": bool(
+ first
+ and (first.get("access") or {}).get("access_addr") == QEMU_REFERENCE["access_addr"]
+ ),
+ "device_map_entries": len(device_map),
+ "device_map": device_map,
+ }
+ )
+ result["wall_s"] = round(time.monotonic() - start, 4)
+ return result
+
+
+# -- S1 integration (defensive) -------------------------------------------
+
+
+def _coerce_device_map(obj) -> DeviceMap | None:
+ """Accept DeviceMap objects, paths, or device-map/profile dicts; else None."""
+ if isinstance(obj, DeviceMap):
+ return obj
+ if isinstance(obj, (str, os.PathLike)):
+ path = os.fspath(obj)
+ if os.path.isfile(path):
+ try:
+ return DeviceMap.load(path)
+ except GreenEyeError:
+ return None
+ return None
+ if isinstance(obj, dict):
+ if obj.get("schema") == "greeneye-device-map/1":
+ try:
+ return DeviceMap.from_dict(obj)
+ except (KeyError, TypeError, ValueError, GreenEyeError):
+ return None
+ if "device_map" in obj:
+ return _coerce_device_map(obj["device_map"])
+ entries = obj.get("entries")
+ if isinstance(entries, list):
+ dm = DeviceMap(meta=dict(obj.get("meta") or {}))
+ try:
+ for raw in entries:
+ dm.add(DeviceEntry.from_dict(raw))
+ except (KeyError, TypeError, ValueError):
+ return None
+ return dm
+ return None
+
+
+def _json_rung(data) -> str | None:
+ if not isinstance(data, dict):
+ return None
+ for key in _RUNG_KEYS:
+ value = data.get(key)
+ if isinstance(value, str) and value in RUNG_INDEX:
+ return value
+ if isinstance(value, dict):
+ inner = value.get("highest") or value.get("rung")
+ if isinstance(inner, str) and inner in RUNG_INDEX:
+ return inner
+ return None
+
+
+def discover_s1_device_model(image: str, sha: str | None = None) -> dict:
+ """Find S1's device model/profile without assuming it exists.
+
+ Returns ``{"device_map": DeviceMap|None, "profile": path|None,
+ "declared_rung": str|None, "source": str|None, "notes": [...]}``.
+ Discovery order: an explicit ``sim.devices`` Python entry point, then JSON
+ files under ``sim/profiles/`` and ``sim/devices/`` (a
+ ``greeneye-sim-profile/1`` profile, a ``greeneye-device-map/1`` file, a
+ profile with a ``device_map`` key, or a raw ``entries`` list). A profile
+ is the semantic device model (``sim.gsp_boot`` runs it); a device map only
+ replays observed accesses. A candidate is preferred when its firmware
+ hash matches the image; otherwise a single candidate is used and noted as
+ unverified pairing.
+ """
+ notes: list[str] = []
+ declared_rung: str | None = None
+
+ try:
+ import sim.devices as devices # type: ignore
+
+ for name in S1_PY_API_NAMES:
+ fn = getattr(devices, name, None)
+ if not callable(fn):
+ continue
+ for args in ((sha,), (image,), ()):
+ if sha is None and args == (sha,):
+ continue
+ try:
+ candidate = fn(*args)
+ except TypeError:
+ continue
+ except (OSError, UnicodeError):
+ # A path-taking helper handed a digest (or a helper handed
+ # the binary firmware) simply does not apply; not an error.
+ break
+ except Exception as exc: # discovery must never break the run
+ notes.append(f"sim.devices.{name}{args!r} failed: {exc}")
+ break
+ if candidate is None:
+ continue
+ dm = _coerce_device_map(candidate)
+ rung = getattr(candidate, "rung", None)
+ if isinstance(rung, str) and rung in RUNG_INDEX:
+ declared_rung = rung
+ if dm is not None:
+ notes.append(f"device map via sim.devices.{name}{args!r}")
+ return {
+ "device_map": dm,
+ "profile": None,
+ "declared_rung": declared_rung,
+ "source": f"sim.devices.{name}",
+ "notes": notes,
+ }
+ except ImportError:
+ notes.append("sim.devices package absent")
+ except Exception as exc:
+ notes.append(f"sim.devices import failed: {exc}")
+
+ candidates: list[tuple[str, DeviceMap, str | None, str | None]] = []
+ profiles: list[tuple[str, str, str | None]] = []
+ for rel in S1_JSON_DIRS:
+ root = os.path.join(TOOL_ROOT, rel)
+ if not os.path.isdir(root):
+ notes.append(f"{rel}/ absent")
+ continue
+ for name in sorted(os.listdir(root)):
+ if not name.endswith(".json"):
+ continue
+ path = os.path.join(root, name)
+ try:
+ with open(path, "r", encoding="utf-8") as fh:
+ data = json.load(fh)
+ except (OSError, ValueError) as exc:
+ notes.append(f"{rel}/{name}: unreadable ({exc})")
+ continue
+ if isinstance(data, dict) and data.get("schema") == "greeneye-sim-profile/1":
+ firmware = data.get("firmware") or {}
+ profiles.append((f"{rel}/{name}", path,
+ str(firmware.get("sha256") or "") or None))
+ continue
+ dm = _coerce_device_map(data)
+ rung = _json_rung(data)
+ if rung and declared_rung is None:
+ declared_rung = rung
+ if dm is not None:
+ candidates.append((f"{rel}/{name}", dm, rung, None))
+
+ exact_profiles = [p for p in profiles if sha and p[2] == sha]
+ if exact_profiles:
+ rel, path, _digest = exact_profiles[0]
+ notes.append(f"using {rel} (greeneye-sim-profile/1, firmware_sha256 matched)")
+ return {
+ "device_map": None,
+ "profile": path,
+ "declared_rung": declared_rung,
+ "source": rel,
+ "notes": notes,
+ }
+
+ if not candidates:
+ if len(profiles) == 1:
+ rel, path, _digest = profiles[0]
+ notes.append(f"using {rel} (single profile, pairing unverified)")
+ return {
+ "device_map": None,
+ "profile": path,
+ "declared_rung": declared_rung,
+ "source": rel,
+ "notes": notes,
+ }
+ notes.append("no S1 device-map/profile JSON found")
+ return {"device_map": None, "profile": None, "declared_rung": declared_rung,
+ "source": None, "notes": notes}
+
+ exact = [
+ c for c in candidates
+ if sha and str((c[1].meta or {}).get("firmware_sha256", "")) == sha
+ ]
+ chosen = exact[0] if exact else candidates[0]
+ note = "firmware_sha256 matched" if exact else "single candidate, pairing unverified"
+ notes.append(f"using {chosen[0]} ({note})")
+ return {
+ "device_map": chosen[1],
+ "profile": None,
+ "declared_rung": chosen[2] or declared_rung,
+ "source": chosen[0],
+ "notes": notes,
+ }
+
+
+# -- native baseline -------------------------------------------------------
+
+
+def estimate_native_rung(run: dict, *, device_model_used: bool,
+ declared_rung: str | None = None) -> tuple[str, str]:
+ """Conservative rung estimate from a bounded native run.
+
+ * matching lenient first touch -> B0 (observed, no device semantics);
+ * a device model that serves the QEMU first-touch and lets execution
+ continue -> B1 (frontier moved past the QEMU fault);
+ * a higher rung only when S1 declares it in profile data (honoured as
+ ``rung_source: declared``).
+
+ B2+ need boot-stage milestones, not just progress; absence of a fault is
+ not evidence of a boot stage. See docs/37 sections 3-4.
+ """
+ stop = run.get("stop") or {}
+ stop_reason = stop.get("reason")
+ first = run.get("first_unknown_mmio")
+ reached_b0 = bool(
+ (first and first.get("addr") == QEMU_REFERENCE["access_addr"])
+ or run.get("instructions", 0) > 0
+ )
+
+ inferred = None
+ basis = None
+ if device_model_used and reached_b0:
+ unmodelled_at_qemu = bool(first and first.get("addr") == QEMU_REFERENCE["access_addr"])
+ if not unmodelled_at_qemu:
+ inferred = "B1"
+ basis = (
+ "device model served the QEMU first-touch and execution continued "
+ "past the QEMU fault to the run bound"
+ )
+ else:
+ inferred = "B0"
+ basis = "device model present but the QEMU first-touch was still served leniently"
+ elif reached_b0:
+ inferred = NATIVE_LENIENT_RUNG
+ basis = "lenient first-touch observation; execution continued past the QEMU fault"
+
+ if inferred is None:
+ return "pre-B0", "no native progress observed"
+
+ rung, source = inferred, "inferred"
+ declared_index = rung_ordinal(declared_rung)
+ if declared_index is not None and declared_index > RUNG_INDEX[inferred]:
+ rung, source = declared_rung, "declared"
+ basis = f"S1 profile declares {declared_rung}; inference was {inferred} ({basis})"
+ return rung, f"{basis} [rung_source={source}, stop={stop_reason}]"
+
+
+def run_native_baseline(
+ image: str = DEFAULT_IMAGE,
+ out_dir: str = DEFAULT_OUT,
+ instructions: int = DEFAULT_INSTRUCTIONS,
+ repeat: int = 2,
+ device_map=None,
+ declared_rung: str | None = None,
+ profile: str | None = None,
+ use_shim: bool = True,
+) -> dict:
+ """Run the bounded deterministic native baseline ``repeat`` times.
+
+ ``profile`` selects the S1 semantic device model (``sim.gsp_boot`` with
+ the versioned profile and the recorded boot shim); without it the run is
+ the lenient ``sim.gsp_profile`` baseline. ``device_map`` remains a
+ legacy/observational MMIO map for the lenient path.
+ """
+ if instructions <= 0 or instructions > MAX_INSTRUCTIONS:
+ raise HarnessError(f"instructions must be in (0, {MAX_INSTRUCTIONS}], got {instructions}")
+ if repeat < 1:
+ raise HarnessError(f"repeat must be >= 1, got {repeat}")
+ if not os.path.isfile(image):
+ return {
+ "backend": "native",
+ "ran": False,
+ "reason": f"firmware absent: {image}",
+ "runs": [],
+ }
+
+ s1_model = bool(profile) or device_map is not None
+ runs: list[dict] = []
+ start_all = time.monotonic()
+ for index in range(repeat):
+ session_dir = os.path.join(out_dir, "sessions", f"native-{index + 1}")
+ _rm_tree(session_dir)
+ start = time.monotonic()
+ if profile:
+ from sim.gsp_boot import run_boot # deferred: only the S1 path needs it
+
+ boot = run_boot(
+ image=image,
+ profile_path=profile,
+ instructions=instructions,
+ record=session_dir,
+ name=f"s4-native-{index + 1}",
+ use_shim=use_shim,
+ )
+ wall = time.monotonic() - start
+ if not boot.ran:
+ raise HarnessError(f"native boot refused to run: {boot.reason}")
+ run = {
+ "instructions": boot.instructions,
+ "cycles": boot.cycles,
+ "cycle_model": boot.cycle_model,
+ "stop": {
+ "reason": boot.stop_reason,
+ "pc": boot.stop_pc,
+ "pc_fw": boot.stop_pc_fw,
+ },
+ "first_unknown_mmio": boot.first_unknown_mmio,
+ "mmio_unknown": boot.mmio_unknown,
+ "device_unhandled": boot.device_unhandled,
+ "device_accesses": boot.device_accesses,
+ "device_state_sha256": boot.device_state_sha256,
+ "milestones": list(boot.milestones),
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "wall_s": round(wall, 4),
+ }
+ run["rung"] = boot.rung
+ run["rung_basis"] = (
+ "observed S1 boot-ladder rung from sim.gsp_boot "
+ f"(milestones={list(boot.milestones)}, mmio_unknown={boot.mmio_unknown}, "
+ f"device_unhandled={boot.device_unhandled})"
+ )
+ else:
+ result = run_profile(
+ image=image,
+ instructions=instructions,
+ record=session_dir,
+ device_map=device_map,
+ name=f"s4-native-{index + 1}",
+ )
+ wall = time.monotonic() - start
+ if not result.ran:
+ raise HarnessError(f"native profile refused to run: {result.reason}")
+ run = {
+ "instructions": result.instructions,
+ "cycles": result.cycles,
+ "cycle_model": result.cycle_model,
+ "stop": {
+ "reason": result.stop_reason,
+ "pc": result.stop_pc,
+ "pc_fw": result.stop_pc_fw,
+ },
+ "first_unknown_mmio": result.first_unknown_mmio,
+ "mmio_unknown": result.mmio_unknown,
+ "session": _relpath(session_dir),
+ "session_hashes": session_hashes(session_dir),
+ "wall_s": round(wall, 4),
+ }
+ run["rung"], run["rung_basis"] = estimate_native_rung(
+ run, device_model_used=s1_model, declared_rung=declared_rung
+ )
+ runs.append(run)
+
+ keys = (
+ "instructions",
+ "cycles",
+ "stop",
+ "first_unknown_mmio",
+ "mmio_unknown",
+ "rung",
+ "device_state_sha256",
+ "milestones",
+ )
+ identical = all(
+ all(a.get(key) == runs[0].get(key) for key in keys) for a in runs[1:]
+ )
+ compare = {"ran": False}
+ if repeat >= 2:
+ try:
+ text = compare_sessions(
+ os.path.join(out_dir, "sessions", "native-1"),
+ os.path.join(out_dir, "sessions", "native-2"),
+ )
+ compare = {
+ "ran": True,
+ "chars": len(text),
+ "headers": [
+ line[3:].strip()
+ for line in text.splitlines()
+ if line.startswith("## ")
+ ],
+ }
+ except Exception as exc: # compare is evidence, never a fatal step
+ compare = {"ran": False, "error": f"{type(exc).__name__}: {exc}"}
+
+ return {
+ "backend": "native",
+ "ran": True,
+ "reason": "ok",
+ "instructions_budget": instructions,
+ "repeat": repeat,
+ "identical": identical,
+ "rung": runs[0]["rung"],
+ "rung_basis": runs[0]["rung_basis"],
+ "runs": runs,
+ "pair_wall_s": round(time.monotonic() - start_all, 4),
+ "compare": compare,
+ "device_model_used": s1_model,
+ "profile": _relpath(profile),
+ }
+
+
+# -- native feature matrix -------------------------------------------------
+
+
+def _events_summary(session_dir: str) -> dict:
+ counts: dict[str, int] = {}
+ path = os.path.join(session_dir, "events.jsonl")
+ try:
+ with open(path, "r", encoding="utf-8") as fh:
+ for line in fh:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ kind = json.loads(line).get("type")
+ except ValueError:
+ continue
+ if kind:
+ counts[kind] = counts.get(kind, 0) + 1
+ except OSError:
+ pass
+ return dict(sorted(counts.items()))
+
+
+def run_feature_matrix(out_dir: str = DEFAULT_OUT) -> dict:
+ """Exercise the native debugger feature set on the bundled demo firmware."""
+ import greeneye as ge
+
+ matrix: dict = {}
+ if not os.path.isfile(DEMO_ELF):
+ return {
+ "ran": False,
+ "reason": f"demo firmware absent: {DEMO_ELF} (run make demo)",
+ "features": {},
+ }
+
+ session_dir = os.path.join(out_dir, "sessions", "features")
+ _rm_tree(session_dir)
+ session = ge.launch(kernel=DEMO_ELF, backend="native", record=session_dir, name="s4-features")
+ try:
+ ctrl = session.symbols.lookup("gsp_ctrl")
+ session.run(duration=0.2)
+ magic = session.memory.read_u32(ctrl)
+ matrix["memory"] = {
+ "status": "pass" if magic == 0x47535031 else "fail",
+ "detail": {"ctrl": ctrl, "magic": magic},
+ }
+
+ with open(DEMO_SCHEMA, "r", encoding="utf-8") as fh:
+ schema = ge.schema_from_json(json.load(fh))
+ decoded = schema.decode(session.memory.read(ctrl, schema.size), base=0)
+ matrix["struct"] = {
+ "status": "pass" if decoded.get("magic") == 0x47535031 else "fail",
+ "detail": {"schema": os.path.basename(DEMO_SCHEMA), "fields": len(decoded)},
+ }
+
+ dispatches: list[int] = []
+ stores: list[int] = []
+ session.hook("rpc_dispatch", on_enter=lambda hit: dispatches.append(hit.arg(0)))
+ session.watch(ctrl + 0x40, size=8, kind="w", callback=lambda hit: stores.append(hit.pc))
+ session.memory.write_u64(ctrl + 0x28, 0x1234)
+ session.memory.write_u64(ctrl + 0x20, 1)
+ session.run(duration=0.25)
+ matrix["hooks"] = {
+ "status": "pass" if dispatches else "fail",
+ "detail": {"rpc_dispatch_hits": len(dispatches), "cmds": dispatches},
+ }
+ matrix["watch"] = {
+ "status": "pass" if stores else "fail",
+ "detail": {"result_field_stores": len(stores), "pcs": stores[:4]},
+ }
+
+ session.memory.write_u64(ctrl + 0x28, 0x55)
+ session.memory.write_u64(ctrl + 0x20, 2)
+ hit = session.run_until("rpc_dispatch", timeout=1.0)
+ bp = session.breakpoint("rpc_dispatch")
+ attached = bool(bp.attached)
+ session.remove_breakpoint(bp)
+ matrix["breakpoints"] = {
+ "status": "pass" if hit is not None and attached and not bp.attached else "fail",
+ "detail": {
+ "run_until_hit": hit.addr if hit is not None else None,
+ "attached_then_removed": attached and not bp.attached,
+ "kind": "sw",
+ },
+ }
+ finally:
+ session.close()
+
+ events = _events_summary(session_dir)
+ matrix["sessions"] = {
+ "status": "pass"
+ if os.path.isfile(os.path.join(session_dir, "session.json"))
+ and os.path.isfile(os.path.join(session_dir, "events.jsonl"))
+ else "fail",
+ "detail": {
+ "dir": _relpath(session_dir),
+ "event_counts": events,
+ "hashes": session_hashes(session_dir),
+ },
+ }
+
+ trace_dir = os.path.join(out_dir, "sessions", "features-trace")
+ _rm_tree(trace_dir)
+ trace_session = ge.launch(
+ kernel=DEMO_ELF,
+ backend="native",
+ record=trace_dir,
+ name="s4-features-trace",
+ trace={"exec": True, "max_events": 4000},
+ )
+ try:
+ trace_session.run(duration=0.1)
+ finally:
+ trace_session.close()
+ trace_path = os.path.join(trace_dir, "blobs", "trace.jsonl")
+ trace_lines = 0
+ if os.path.isfile(trace_path):
+ with open(trace_path, "r", encoding="utf-8", errors="replace") as fh:
+ trace_lines = sum(1 for _ in fh)
+ matrix["trace"] = {
+ "status": "pass" if trace_lines > 0 else "fail",
+ "detail": {
+ "dir": _relpath(trace_dir),
+ "blob": _relpath(trace_path),
+ "events": trace_lines,
+ "max_events": 4000,
+ "hashes": session_hashes(trace_dir),
+ },
+ }
+
+ if os.path.isfile(SDK_ELF):
+ from greeneye.inject import invoke_remote, load_service
+
+ inject_dir = os.path.join(out_dir, "sessions", "features-inject")
+ _rm_tree(inject_dir)
+ inject_session = ge.launch(
+ kernel=DEMO_ELF, backend="native", record=inject_dir, name="s4-features-inject"
+ )
+ try:
+ inject_session.run(duration=0.25)
+ service = load_service(inject_session, SDK_ELF, slot=6)
+ value = invoke_remote(inject_session, service, 1, 0x41)
+ finally:
+ inject_session.close()
+ matrix["injection"] = {
+ "status": "pass" if value == (0x41 ^ 0xBEEF) else "fail",
+ "detail": {
+ "dir": _relpath(inject_dir),
+ "blob": _relpath(SDK_ELF),
+ "slot": 6,
+ "ping_result": value,
+ "expected": 0x41 ^ 0xBEEF,
+ "hashes": session_hashes(inject_dir),
+ },
+ }
+ else:
+ matrix["injection"] = {
+ "status": "not_run",
+ "reason": "sdk/build/injected_demo.elf not built (run make sdk)",
+ }
+
+ matrix["mcp"] = {
+ "status": "not_run",
+ "reason": (
+ "native MCP surface is S3's deliverable (plugins/mcp_server.py); "
+ "S4 must not race that work. Re-run after S3 lands (docs/37)."
+ ),
+ }
+ analyze_dir = os.path.join(out_dir, "sessions", "native-1")
+ compare_dir = os.path.join(out_dir, "sessions", "native-2")
+ if os.path.isfile(os.path.join(analyze_dir, "session.json")) and os.path.isfile(
+ os.path.join(compare_dir, "session.json")
+ ):
+ try:
+ from greeneye.analyze import render_summary
+
+ summary = render_summary(analyze_dir)
+ delta_text = compare_sessions(analyze_dir, compare_dir)
+ matrix["analyze_compare"] = {
+ "status": "pass" if summary and delta_text else "fail",
+ "detail": {
+ "analyze_session": _relpath(analyze_dir),
+ "analyze_chars": len(summary),
+ "compare_pair": [_relpath(analyze_dir), _relpath(compare_dir)],
+ "compare_chars": len(delta_text),
+ "compare_headers": [
+ line[3:].strip()
+ for line in delta_text.splitlines()
+ if line.startswith("## ")
+ ],
+ },
+ }
+ except Exception as exc: # evidence step, never fatal to the matrix
+ matrix["analyze_compare"] = {
+ "status": "fail",
+ "reason": f"{type(exc).__name__}: {exc}",
+ }
+ else:
+ matrix["analyze_compare"] = {
+ "status": "not_run",
+ "reason": "native-1/native-2 sessions absent",
+ }
+ return {"ran": True, "reason": "ok", "features": matrix}
+
+
+# -- report ----------------------------------------------------------------
+
+
+def threshold_checks(report: dict, thresholds: dict | None = None) -> dict:
+ """Evaluate the frozen thresholds against a report; returns check -> bool."""
+ th = dict(FROZEN_THRESHOLDS)
+ th.update(thresholds or {})
+ qemu = report.get("qemu") or {}
+ native = report.get("native") or {}
+ checks: dict = {
+ "qemu_first_fault_reproduced": bool(
+ qemu.get("ran") and qemu.get("first_touch_match")
+ ),
+ "native_above_qemu": above_qemu_line(
+ qemu.get("rung"), native.get("rung"), int(th["native_rungs_above_qemu"])
+ ),
+ "identical_runs": bool(
+ native.get("ran")
+ and native.get("repeat", 0) >= int(th["identical_runs"])
+ and native.get("identical")
+ ),
+ "native_pair_wall_s": bool(
+ native.get("ran")
+ and float(native.get("pair_wall_s", 1e9)) <= float(th["native_pair_wall_s"])
+ ),
+ "device_model_documented": bool(
+ not th.get("device_model_required", True) or native.get("device_model_used")
+ ),
+ }
+ checks["all"] = all(checks.values())
+ return checks
+
+
+def build_report(
+ *,
+ image: str,
+ qemu: dict,
+ native: dict,
+ features: dict | None,
+ s1: dict,
+ thresholds: dict,
+) -> dict:
+ checks = threshold_checks({"qemu": qemu, "native": native}, thresholds)
+ rung_progress = bool(checks["native_above_qemu"] and checks["identical_runs"])
+ if not qemu.get("ran"):
+ verdict = "unverified_qemu_absent"
+ elif not checks["qemu_first_fault_reproduced"]:
+ verdict = "unverified_qemu_mismatch"
+ elif checks["all"]:
+ verdict = "above"
+ elif rung_progress and not checks["device_model_documented"]:
+ # Rung progress exists, but docs/35 1.1 also requires working device
+ # semantics; lenient MMIO cannot satisfy the line contract. Underclaim.
+ verdict = "above_lenient_only"
+ else:
+ verdict = "not_above"
+ return {
+ "schema": SCHEMA,
+ "harness": "harness/sim_qemu_line.py",
+ "line_definition": "docs/35-GREENEYE-SIMULATOR-DESIGN.md 1.1 + 3.3",
+ "firmware": {
+ "name": os.path.basename(image),
+ "sha256": firmware_sha256(image) if os.path.isfile(image) else None,
+ },
+ "rung_order": list(RUNG_ORDER),
+ "thresholds": thresholds,
+ "threshold_checks": checks,
+ "verdict": verdict,
+ "verdict_explanation": {
+ "above": "all frozen checks passed: QEMU fault reproduced, native >= QEMU + 1 rung, identical runs, wall bound, documented device model",
+ "above_lenient_only": "native progressed past the QEMU fault, but lenient MMIO is not a documented device model (docs/35 1.1) - contract NOT satisfied",
+ "not_above": "one or more frozen checks failed and no rung-progress excuse applies",
+ "unverified_qemu_absent": "QEMU baseline unavailable; no A/B comparison is possible",
+ "unverified_qemu_mismatch": "QEMU ran but did not reproduce its documented first fault; the rungs are not comparable",
+ }.get(verdict, ""),
+ "rung_progress": rung_progress,
+ "above_qemu_line": verdict == "above",
+ "qemu": {k: v for k, v in qemu.items() if k != "device_map"},
+ "native": native,
+ "features": features,
+ "s1_integration": {
+ "source": s1.get("source"),
+ "profile": _relpath(s1.get("profile")),
+ "declared_rung": s1.get("declared_rung"),
+ "notes": s1.get("notes", []),
+ },
+ "x86_vms_running": _x86_vms_running(),
+ "not_verified": [
+ "lenient MMIO (B0) is observation, not device semantics",
+ "B3/B4 are not reached: the S1 device model stops at the B2 DMEM-runspace unknown",
+ "the boot shim values (bootargs/WPR meta/DMEM mirror) are declared assumptions",
+ "MCP on native is S3-owned and not exercised here",
+ "L2 timing: cycles are modeled (docs/32), never silicon claims",
+ "no hardware/GPU/guest/module involvement by design",
+ ],
+ "rerun_when_s1_lands": [
+ "python3 harness/sim_qemu_line.py",
+ "S1 landed (2026-10-03): sim/profiles/*.json is discovered and the native side runs sim.gsp_boot",
+ "sim/devices/*.json with greeneye-device-map/1 (or a device_map key) is still picked up as an observational map",
+ ],
+ }
+
+
+def render_markdown(report: dict) -> str:
+ qemu = report.get("qemu") or {}
+ native = report.get("native") or {}
+ lines = [
+ "# Simulator vs the QEMU line - measured report",
+ "",
+ f"* schema: `{report.get('schema')}`",
+ f"* harness: `{report.get('harness')}`",
+ f"* line: {report.get('line_definition')}",
+ f"* firmware: `{(report.get('firmware') or {}).get('name')}` "
+ f"sha256 `{((report.get('firmware') or {}).get('sha256') or '')[:16]}...`",
+ f"* verdict: **{report.get('verdict')}** "
+ f"(above_qemu_line={report.get('above_qemu_line')})",
+ f"* verdict meaning: {report.get('verdict_explanation') or '-'}",
+ "",
+ "## Rung table",
+ "",
+ "| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence |",
+ "|---|---|---|---:|---:|---|---:|---|",
+ ]
+ runs = native.get("runs") or []
+ lines.append(
+ f"| QEMU probe | {qemu.get('ran')} | {qemu.get('rung')} | - | - | "
+ f"{'fault' if qemu.get('first_fault') else '-'} | {qemu.get('wall_s')} | "
+ f"`{qemu.get('session') or qemu.get('reason')}` |"
+ )
+ for index, run in enumerate(runs, start=1):
+ stop = run.get("stop") or {}
+ pc_fw = stop.get("pc_fw")
+ pc_fw_text = f"{pc_fw:#x}" if pc_fw is not None else "-"
+ lines.append(
+ f"| native run {index} | True | {run.get('rung')} | {run.get('instructions')} | "
+ f"{run.get('cycles')} | {stop.get('reason')}@{pc_fw_text} | "
+ f"{run.get('wall_s')} | `{run.get('session')}` |"
+ )
+ if not runs:
+ lines.append(f"| native | {native.get('ran')} | - | - | - | - | - | {native.get('reason')} |")
+
+ first = qemu.get("first_fault") or {}
+ lines += [
+ "",
+ "### QEMU first fault",
+ "",
+ f"* {first.get('desc')} at fw vaddr `{first.get('epc_fw') if first.get('epc_fw') is not None else 0:#x}` "
+ f"accessing `{(first.get('access_addr') or 0):#x}` ({first.get('size')} bytes, {first.get('rw')})",
+ f"* first-touch matches the documented reference: {qemu.get('first_touch_match')}",
+ f"* fault records in the capped log window: {qemu.get('fault_count_window')}",
+ "",
+ "## Frozen thresholds",
+ "",
+ "| check | pass |",
+ "|---|---|",
+ ]
+ for name, value in (report.get("threshold_checks") or {}).items():
+ lines.append(f"| {name} | {value} |")
+ lines += [
+ "",
+ f"Thresholds (frozen {report['thresholds'].get('frozen_on')}): "
+ f"native >= QEMU + {report['thresholds'].get('native_rungs_above_qemu')} rung(s); "
+ f"{report['thresholds'].get('identical_runs')} identical runs; "
+ f"pair wall <= {report['thresholds'].get('native_pair_wall_s')} s.",
+ "",
+ "## Determinism",
+ "",
+ f"* identical across {native.get('repeat')} native runs: {native.get('identical')}",
+ f"* session compare ran: {(native.get('compare') or {}).get('ran')} "
+ f"({(native.get('compare') or {}).get('headers')})",
+ "",
+ "## Session hashes (sha256)",
+ "",
+ "| session | file | sha256 |",
+ "|---|---|---|",
+ ]
+ hash_rows = [("qemu-probe", qemu.get("session"), qemu.get("session_hashes") or {})]
+ for index, run in enumerate(runs, start=1):
+ hash_rows.append((f"native-{index}", run.get("session"), run.get("session_hashes") or {}))
+ features_detail = ((report.get("features") or {}).get("features") or {})
+ for feature in ("sessions", "trace", "injection"):
+ detail = (features_detail.get(feature) or {}).get("detail") or {}
+ if isinstance(detail, dict):
+ hashes = detail.get("hashes") or {}
+ if hashes:
+ hash_rows.append((feature, detail.get("dir") or detail.get("blob"), hashes))
+ for label, session, hashes in hash_rows:
+ if not hashes:
+ lines.append(f"| {label} | - | (no hashes recorded) |")
+ continue
+ for name in sorted(hashes):
+ lines.append(f"| {label} | {name} | `{hashes[name]}` |")
+ lines += [
+ "",
+ "## Native feature matrix (demo firmware)",
+ "",
+ "| feature | status | detail |",
+ "|---|---|---|",
+ ]
+ feature_block = (report.get("features") or {}).get("features") or {}
+ for name in sorted(feature_block):
+ entry = feature_block[name]
+ detail = entry.get("detail") or entry.get("reason") or ""
+ lines.append(f"| {name} | {entry.get('status')} | {detail} |")
+ if not feature_block:
+ lines.append(f"| - | not_run | {(report.get('features') or {}).get('reason')} |")
+ lines += [
+ "",
+ "## S1 integration",
+ "",
+ f"* device source: {report['s1_integration'].get('source') or 'lenient (no S1 device model found)'}",
+ f"* profile: {report['s1_integration'].get('profile') or '-'}",
+ f"* declared rung: {report['s1_integration'].get('declared_rung')}",
+ ]
+ for note in report["s1_integration"].get("notes") or []:
+ lines.append(f"* note: {note}")
+ lines += [
+ "",
+ "## Not verified",
+ "",
+ ]
+ for item in report.get("not_verified") or []:
+ lines.append(f"* {item}")
+ lines += [
+ "",
+ "## S1 integration notes",
+ "",
+ ]
+ for item in report.get("rerun_when_s1_lands") or []:
+ lines.append(f"* `{item}`" if item.startswith("python3") else f"* {item}")
+ lines += [
+ "",
+ f"* other agents' x86 QEMU VMs observed (never touched): {report.get('x86_vms_running')}",
+ "",
+ ]
+ return "\n".join(lines)
+
+
+def run_harness(
+ *,
+ image: str = DEFAULT_IMAGE,
+ out_dir: str = DEFAULT_OUT,
+ instructions: int = DEFAULT_INSTRUCTIONS,
+ qemu_duration: float = DEFAULT_QEMU_DURATION_S,
+ repeat: int = 2,
+ device_map: str | None = None,
+ features: bool = True,
+ thresholds: dict | None = None,
+ run_qemu: bool = True,
+) -> dict:
+ """Run the full A/B harness and write ``sim-qemu-line.json`` + ``.md``."""
+ os.makedirs(out_dir, exist_ok=True)
+ frozen = dict(FROZEN_THRESHOLDS)
+ frozen.update(thresholds or {})
+
+ if device_map is not None:
+ dm = DeviceMap.load(device_map)
+ s1 = {
+ "device_map": dm,
+ "profile": None,
+ "declared_rung": None,
+ "source": _relpath(device_map),
+ "notes": ["explicit --device-map override"],
+ }
+ else:
+ sha = firmware_sha256(image) if os.path.isfile(image) else None
+ s1 = discover_s1_device_model(image, sha)
+
+ qemu = (
+ run_qemu_baseline(image=image, out_dir=out_dir, duration=qemu_duration)
+ if run_qemu
+ else {"backend": "qemu", "ran": False, "reason": "qemu=False", "rung": None}
+ )
+
+ native = run_native_baseline(
+ image=image,
+ out_dir=out_dir,
+ instructions=instructions,
+ repeat=repeat,
+ device_map=s1.get("device_map"),
+ declared_rung=s1.get("declared_rung"),
+ profile=s1.get("profile"),
+ )
+ if not native.get("ran"):
+ report = build_report(
+ image=image, qemu=qemu, native=native, features=None, s1=s1, thresholds=frozen
+ )
+ else:
+ feature_result = run_feature_matrix(out_dir) if features else None
+ report = build_report(
+ image=image,
+ qemu=qemu,
+ native=native,
+ features=feature_result,
+ s1=s1,
+ thresholds=frozen,
+ )
+
+ json_path = _write_json(os.path.join(out_dir, "sim-qemu-line.json"), report)
+ md_path = _write_text(os.path.join(out_dir, "sim-qemu-line.md"), render_markdown(report))
+ report["report_json"] = _relpath(json_path)
+ report["report_md"] = _relpath(md_path)
+ return report
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(
+ prog="python3 harness/sim_qemu_line.py",
+ description=(
+ "S4 A/B: QEMU probe vs native GSP simulator on the real 570.144 image; "
+ "emits sim-qemu-line.json + .md. Bounded, no hardware."
+ ),
+ )
+ parser.add_argument("--image", default=DEFAULT_IMAGE, help="flat GSP firmware image")
+ parser.add_argument("--out", default=DEFAULT_OUT, help="report directory")
+ parser.add_argument(
+ "--instructions", type=int, default=DEFAULT_INSTRUCTIONS,
+ help=f"native instruction budget per run (<= {MAX_INSTRUCTIONS})",
+ )
+ parser.add_argument(
+ "--qemu-duration", type=float, default=DEFAULT_QEMU_DURATION_S,
+ help=f"QEMU wall-clock probe bound (<= {MAX_QEMU_DURATION_S}s)",
+ )
+ parser.add_argument("--repeat", type=int, default=2, help="native runs for determinism")
+ parser.add_argument("--device-map", help="explicit greeneye-device-map/1 file (skips discovery)")
+ parser.add_argument("--no-qemu", action="store_true", help="native only (no baseline claim)")
+ parser.add_argument("--no-features", action="store_true", help="skip the feature matrix")
+ parser.add_argument("--json", action="store_true", help="print the report JSON")
+ args = parser.parse_args(argv)
+
+ try:
+ report = run_harness(
+ image=args.image,
+ out_dir=args.out,
+ instructions=args.instructions,
+ qemu_duration=args.qemu_duration,
+ repeat=args.repeat,
+ device_map=args.device_map,
+ features=not args.no_features,
+ run_qemu=not args.no_qemu,
+ )
+ except (HarnessError, GreenEyeError) as exc:
+ print(f"[sim-qemu-line] error: {exc}", file=sys.stderr)
+ return 1
+
+ if not os.path.isfile(args.image):
+ print(f"[sim-qemu-line] skipped: firmware absent: {args.image}", file=sys.stderr)
+ return 2
+ if args.json:
+ print(json.dumps(report, indent=1, sort_keys=True, default=str))
+ native = report.get("native") or {}
+ print(f"[sim-qemu-line] qemu rung: {(report.get('qemu') or {}).get('rung')}")
+ print(f"[sim-qemu-line] native rung: {native.get('rung')} (identical={native.get('identical')})")
+ print(f"[sim-qemu-line] verdict: {report.get('verdict')}")
+ print(f"[sim-qemu-line] thresholds: {report.get('threshold_checks')}")
+ print(f"[sim-qemu-line] report: {report.get('report_json')} + {report.get('report_md')}")
+ return 0 if report.get("above_qemu_line") else 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())