PPysolate SpineREAD THE CODE · UNDERSTAND THE SYSTEM
课程与章节
源码索引 / CURRENT IMPLEMENTATION

learning/deep-probes/ast_transform.py

209 行 · 点击行号可定位;函数链接随声明位置更新,不手填行号。

跳到函数 / 声明
from __future__ import annotations

import ast
import hashlib
import json
import platform
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "guest"))
from plm import transform  # type: ignore[reportMissingImports]  # noqa: E402


CASES = {
    "independent": (
        'a = tool("lookup", key="book")\n'
        'b = tool("lookup", key="shipping")\n'
        'result = a + b\n',
        {},
    ),
    "dependency": (
        'key = tool("lookup", key="choose")\n'
        'price = tool("lookup", key=key)\n'
        'shipping = tool("lookup", key="shipping")\n'
        'result = price + shipping\n',
        {},
    ),
    "branch_false": (
        'if inputs["yes"]:\n'
        '    a = tool("lookup", key="book")\n'
        'else:\n'
        '    a = tool("lookup", key="shipping")\n'
        'result = a\n',
        {"yes": False},
    ),
    "exception": (
        'try:\n'
        '    a = tool("lookup", key="book")\n'
        '    b = tool("lookup", key=inputs["absent"])\n'
        'except KeyError:\n'
        '    result = a\n',
        {},
    ),
    "unsupported_loop": (
        'result = 0\n'
        'for i in range(2):\n'
        '    result += tool("lookup", key="book")\n',
        {},
    ),
    "freshname": (
        '_spine_prepare = "user value"\n'
        'a = tool("lookup", key="book")\n'
        'result = _spine_prepare\n',
        {},
    ),
}

ADMISSION = {
    "constant_argument": 'a = tool("lookup", key="book")\n',
    "input_subscript_argument": 'a = tool("lookup", key=inputs["item"])\n',
    "data_assignment_then_tool": 'key = 1 + 2 * 3\na = tool("lookup", key=key)\n',
    "dynamic_subscript_rejected": 'a = tool("lookup", key=inputs[inputs["index"]])\n',
    "attribute_rejected": 'obj = 1\na = tool("lookup", key=obj.real)\n',
    "call_argument_rejected": 'f = 1\na = tool("lookup", key=f())\n',
    "kwargs_unpack_rejected": 'a = tool("lookup", **inputs)\n',
    "extra_positional_rejected": 'a = tool("lookup", "extra", key="book")\n',
    "multi_target_rejected": 'a = b = tool("lookup", key="book")\n',
    "loop_rejected": 'for x in []:\n    a = tool("lookup", key="book")\n',
    "import_rejected": 'import os\na = tool("lookup", key="book")\n',
    "inputs_rebind_rejected": 'inputs = {}\na = tool("lookup", key="book")\n',
    "tool_rebind_rejected": 'tool = 1\na = tool("lookup", key="book")\n',
}

VALUES = {"book": 21, "shipping": 5, "choose": "book"}


def execute_transformed(source: str, inputs: dict[str, object]):
    events: list[tuple[object, ...]] = []
    values = dict(VALUES)

    def tool(name: str, **args):
        key = args["key"]
        events.append(("call", key))
        return values[key]

    def prepare(thunk):
        try:
            name, args = thunk()
        except Exception as error:
            # The production helper returns 0 without recording this diagnostic.
            events.append(("prepare-error", type(error).__name__))
            return 0
        events.append(("prepare", args["key"]))
        return name, args

    def resolve(token, name: str, **args):
        if token == 0:
            return tool(name, **args)
        events.append(("resolve", args["key"]))
        return values[args["key"]]

    before_tree = ast.parse(source, filename="<deep-ast-before>")
    tree, helpers = transform(source)
    scope = {"__name__": "__main__", "inputs": inputs, "tool": tool}
    if helpers:
        scope.update(zip(helpers, (prepare, resolve)))
    try:
        exec(compile(tree, "<deep-ast-after>", "exec"), scope)
        result = scope.get("result")
        error = None
    except BaseException as exc:  # record, do not hide, an unexpected case failure
        result = None
        error = f"{type(exc).__name__}: {exc}"
    return {
        "before_source": ast.unparse(before_tree),
        "after_source": ast.unparse(tree),
        "before_dump": ast.dump(before_tree, indent=2),
        "after_dump": ast.dump(tree, indent=2),
        "helpers": list(helpers) if helpers else None,
        "changed": helpers is not None,
        "result": result,
        "error": error,
        "events": events,
        "dump_equal": ast.dump(tree) == ast.dump(before_tree),
    }


def write_case(name: str, source: str, inputs: dict[str, object]) -> dict[str, object]:
    out = execute_transformed(source, inputs)
    case_dir = ROOT / ".local" / "deep" / "ast" / name
    case_dir.mkdir(parents=True, exist_ok=True)
    (case_dir / "before.py").write_text(source, encoding="utf-8")
    (case_dir / "after.py").write_text(out["after_source"] + "\n", encoding="utf-8")
    (case_dir / "before.ast.txt").write_text(out["before_dump"] + "\n", encoding="utf-8")
    (case_dir / "after.ast.txt").write_text(out["after_dump"] + "\n", encoding="utf-8")
    (case_dir / "run.json").write_text(
        json.dumps(
            {
                "helpers": out["helpers"],
                "changed": out["changed"],
                "result": out["result"],
                "error": out["error"],
                "events": out["events"],
                "dump_equal": out["dump_equal"],
                "inputs": inputs,
            },
            ensure_ascii=False,
            indent=2,
        )
        + "\n",
        encoding="utf-8",
    )
    return {
        "name": name,
        "inputs": inputs,
        "before": out["before_source"],
        "after": out["after_source"],
        "helpers": out["helpers"],
        "changed": out["changed"],
        "result": out["result"],
        "error": out["error"],
        "events": out["events"],
        "dump_equal": out["dump_equal"],
        "before_ast": str((case_dir / "before.ast.txt").relative_to(ROOT)),
        "after_ast": str((case_dir / "after.ast.txt").relative_to(ROOT)),
    }


def inspect_admission(name: str, source: str) -> dict[str, object]:
    before = ast.parse(source, filename=f"<admission:{name}:before>")
    after, helpers = transform(source)
    return {
        "name": name,
        "source": source,
        "changed": helpers is not None,
        "helpers": list(helpers) if helpers else None,
        "dump_equal": ast.dump(before) == ast.dump(after),
        "after_source": ast.unparse(after),
    }


if __name__ == "__main__":
    cases = [write_case(name, source, inputs) for name, (source, inputs) in CASES.items()]
    expected = {"independent":26,"dependency":26,"branch_false":5,"exception":21,"unsupported_loop":42,"freshname":"user value"}
    assert all(case["error"] is None and case["result"] == expected[case["name"]] for case in cases), cases
    admission = [inspect_admission(name, source) for name, source in ADMISSION.items()]
    admitted = {"constant_argument", "input_subscript_argument", "data_assignment_then_tool"}
    assert all(item["changed"] == (item["name"] in admitted) for item in admission), admission
    admission_path = ROOT / ".local" / "deep" / "ast" / "admission.json"
    admission_path.write_text(json.dumps(admission, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    manifest = {
        "repository": ".",
        "reviewed_revision": "190ea2e238bba454cdcc335e95736c3f27f6be9a",
        "source_sha256": hashlib.sha256((ROOT / "guest" / "plm.py").read_bytes()).hexdigest(),
        "python": sys.version,
        "platform": platform.platform(),
        "module": "guest/plm.py",
        "cases": cases,
        "admission": str(admission_path.relative_to(ROOT)),
    }
    target = ROOT / ".local" / "deep" / "ast" / "manifest.json"
    target.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    for case in cases:
        print(f"[{case['name']}] changed={case['changed']} helpers={case['helpers']} result={case['result']!r} error={case['error']!r}")
        print(f"  events={case['events']}")
        print(f"  dump_equal={case['dump_equal']}")
    for item in admission:
        print(f"[admission:{item['name']}] changed={item['changed']} helpers={item['helpers']} dump_equal={item['dump_equal']}")