345 lines
18 KiB
Python
345 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""Guard C89 declarations and writer error order in GenericAlias repr."""
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import NoReturn
|
|
|
|
|
|
def fail(message: str) -> NoReturn:
|
|
print(message, file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def extract_function(source: str, signature: str) -> str:
|
|
start = source.find(signature)
|
|
if start < 0:
|
|
fail(f"missing function signature: {signature}")
|
|
brace = source.find("{", start + len(signature))
|
|
if brace < 0:
|
|
fail("missing function body")
|
|
depth = 0
|
|
for end in range(brace, len(source)):
|
|
if source[end] == "{":
|
|
depth += 1
|
|
elif source[end] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[brace + 1:end]
|
|
fail("unterminated function body")
|
|
|
|
|
|
source = Path(sys.argv[1]).read_text(encoding="utf-8")
|
|
if "static PyMappingMethods ga_as_mapping = {\n 0,\n ga_getitem,\n 0,\n};" not in source:
|
|
fail("ga_as_mapping must use VC6-compatible positional fields in PyMappingMethods order")
|
|
body = extract_function(source, "ga_repr(PyObject *self)")
|
|
if "Py_ssize_t i;" not in body or "for (Py_ssize_t i =" in body:
|
|
fail("ga_repr loop index must be declared before executable statements")
|
|
if "PyObject *p;" not in body or "PyObject *p =" in body:
|
|
fail("ga_repr tuple item must be declared at the loop-block entry")
|
|
ordered = (
|
|
"_PyUnicodeWriter_Init(&writer);",
|
|
"if (alias->starred)",
|
|
"if (ga_repr_item(&writer, alias->origin) < 0)",
|
|
"if (_PyUnicodeWriter_WriteASCIIString(&writer, \"[\", 1) < 0)",
|
|
"for (i = 0; i < len; i++)",
|
|
"if (i > 0)",
|
|
"p = PyTuple_GET_ITEM(alias->args, i);",
|
|
"if (ga_repr_item(&writer, p) < 0)",
|
|
"if (len == 0)",
|
|
"if (_PyUnicodeWriter_WriteASCIIString(&writer, \"]\", 1) < 0)",
|
|
"return _PyUnicodeWriter_Finish(&writer);",
|
|
"_PyUnicodeWriter_Dealloc(&writer);",
|
|
)
|
|
positions = [body.find(token) for token in ordered]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_repr must preserve writer and tuple-iteration error order")
|
|
|
|
tuple_index = extract_function(source, "tuple_index(PyObject *self, Py_ssize_t len, PyObject *item)")
|
|
if not tuple_index.lstrip().startswith("Py_ssize_t i;") or "for (Py_ssize_t i =" in tuple_index:
|
|
fail("tuple_index loop variable must be declared at function entry")
|
|
if not tuple_index.find("for (i = 0; i < len; i++)") < tuple_index.find("if (PyTuple_GET_ITEM(self, i) == item)") < tuple_index.find("return -1;"):
|
|
fail("tuple_index must preserve scan and not-found behavior")
|
|
|
|
tuple_extend = extract_function(source, "tuple_extend(PyObject **dst, Py_ssize_t dstindex,\n PyObject **src, Py_ssize_t count)")
|
|
if not tuple_extend.lstrip().startswith("Py_ssize_t i;") or "for (Py_ssize_t i =" in tuple_extend:
|
|
fail("tuple_extend loop index must be declared at function entry")
|
|
if "PyObject *item;" not in tuple_extend or "PyObject *item =" in tuple_extend:
|
|
fail("tuple_extend item local must be declared at loop-block entry")
|
|
ordered_extend = (
|
|
"assert(count >= 0);",
|
|
"if (_PyTuple_Resize(dst, PyTuple_GET_SIZE(*dst) + count - 1) != 0)",
|
|
"assert(dstindex + count <= PyTuple_GET_SIZE(*dst));",
|
|
"for (i = 0; i < count; ++i)",
|
|
"item = src[i];",
|
|
"Py_INCREF(item);",
|
|
"PyTuple_SET_ITEM(*dst, dstindex + i, item);",
|
|
"return dstindex + count;",
|
|
)
|
|
positions = [tuple_extend.find(token) for token in ordered_extend]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("tuple_extend must preserve resize, retain, and write order")
|
|
|
|
make_parameters = extract_function(source, "_Py_make_parameters(PyObject *args)")
|
|
entry_declarations = (
|
|
"Py_ssize_t nargs;",
|
|
"Py_ssize_t len;",
|
|
"PyObject *parameters;",
|
|
"Py_ssize_t iparam;",
|
|
"Py_ssize_t iarg;",
|
|
)
|
|
remaining = make_parameters.lstrip()
|
|
for declaration in entry_declarations:
|
|
if not remaining.startswith(declaration + "\n"):
|
|
fail("_Py_make_parameters entry declaration missing or out of order: " + declaration)
|
|
remaining = remaining[len(declaration):].lstrip()
|
|
if "for (Py_ssize_t iarg =" in make_parameters or "Py_ssize_t iparam =" in make_parameters:
|
|
fail("_Py_make_parameters must not declare function-scope locals after statements")
|
|
if "Py_ssize_t j;" not in make_parameters or "for (Py_ssize_t j =" in make_parameters:
|
|
fail("_Py_make_parameters nested loop index must be declared at its block entry")
|
|
|
|
subs_tvars = extract_function(source, "subs_tvars(PyObject *obj, PyObject *params,\n PyObject **argitems, Py_ssize_t nargs)")
|
|
if "Py_ssize_t j;" not in subs_tvars or "Py_ssize_t i;" not in subs_tvars:
|
|
fail("subs_tvars loop indices must be declared at the tuple-processing block entry")
|
|
if "Py_ssize_t j =" in subs_tvars or "for (Py_ssize_t i =" in subs_tvars:
|
|
fail("subs_tvars must not initialize indices after statements or in for headers")
|
|
ordered_subs = (
|
|
"Py_ssize_t nparams = PyTuple_GET_SIZE(params);",
|
|
"Py_ssize_t nsubargs = PyTuple_GET_SIZE(subparams);",
|
|
"PyObject *subargs = PyTuple_New(nsubargs);",
|
|
"if (subargs == NULL)",
|
|
"j = 0;",
|
|
"for (i = 0; i < nsubargs; ++i)",
|
|
"PyObject *arg = PyTuple_GET_ITEM(subparams, i);",
|
|
"Py_ssize_t iparam = tuple_index(params, nparams, arg);",
|
|
"if (iparam >= 0)",
|
|
"Py_INCREF(arg);",
|
|
"PyTuple_SET_ITEM(subargs, j, arg);",
|
|
"Py_DECREF(subargs);",
|
|
"Py_XDECREF(subparams);",
|
|
"return obj;",
|
|
)
|
|
positions = [subs_tvars.find(token) for token in ordered_subs]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("subs_tvars must preserve tuple allocation, substitution, and cleanup order")
|
|
|
|
is_unpacked = extract_function(source, "_is_unpacked_typevartuple(PyObject *arg)")
|
|
if not is_unpacked.lstrip().startswith("PyObject *tmp;\n int res;") or "int res =" in is_unpacked:
|
|
fail("_is_unpacked_typevartuple locals must be declared at function entry")
|
|
ordered_is_unpacked = (
|
|
"if (PyType_Check(arg))",
|
|
"res = _PyObject_LookupAttr(arg, &_Py_ID(__typing_is_unpacked_typevartuple__), &tmp);",
|
|
"if (res > 0)",
|
|
"res = PyObject_IsTrue(tmp);",
|
|
"Py_DECREF(tmp);",
|
|
"return res;",
|
|
)
|
|
positions = [is_unpacked.find(token) for token in ordered_is_unpacked]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("_is_unpacked_typevartuple must preserve type-check, lookup, and cleanup order")
|
|
|
|
unpack_args = extract_function(source, "_unpack_args(PyObject *item)")
|
|
entry_unpacked = ("PyObject *newargs;", "int is_tuple;", "Py_ssize_t nitems;", "PyObject **argitems;", "Py_ssize_t i;")
|
|
remaining = unpack_args.lstrip()
|
|
for declaration in entry_unpacked:
|
|
if not remaining.startswith(declaration + "\n"):
|
|
fail("_unpack_args entry declaration missing or out of order: " + declaration)
|
|
remaining = remaining[len(declaration):].lstrip()
|
|
if "for (Py_ssize_t i =" in unpack_args:
|
|
fail("_unpack_args must not declare its index in the for initializer")
|
|
ordered_unpack = (
|
|
"newargs = PyList_New(0);",
|
|
"if (newargs == NULL)",
|
|
"is_tuple = PyTuple_Check(item);",
|
|
"nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1;",
|
|
"argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item;",
|
|
"for (i = 0; i < nitems; i++)",
|
|
"item = argitems[i];",
|
|
"if (!PyType_Check(item))",
|
|
"PyList_Append(newargs, item)",
|
|
"Py_SETREF(newargs, PySequence_Tuple(newargs));",
|
|
"return newargs;",
|
|
)
|
|
positions = [unpack_args.find(token) for token in ordered_unpack]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("_unpack_args must preserve list allocation, element processing, and tuple conversion order")
|
|
|
|
subs_parameters = extract_function(source, "_Py_subs_parameters(PyObject *self, PyObject *args, PyObject *parameters, PyObject *item)")
|
|
entry_subs = ("Py_ssize_t nparams;", "Py_ssize_t i;", "int is_tuple;", "Py_ssize_t nitems;", "PyObject **argitems;", "Py_ssize_t nargs;", "PyObject *newargs;", "Py_ssize_t iarg;", "Py_ssize_t jarg;")
|
|
remaining = subs_parameters.lstrip()
|
|
for declaration in entry_subs:
|
|
if not remaining.startswith(declaration + "\n"):
|
|
fail("_Py_subs_parameters entry declaration missing or out of order: " + declaration)
|
|
remaining = remaining[len(declaration):].lstrip()
|
|
if "for (Py_ssize_t i =" in subs_parameters or "for (Py_ssize_t iarg =" in subs_parameters:
|
|
fail("_Py_subs_parameters must not declare loop indices in for initializers")
|
|
ordered_subs_parameters = (
|
|
"nparams = PyTuple_GET_SIZE(parameters);",
|
|
"if (nparams == 0)",
|
|
"item = _unpack_args(item);",
|
|
"for (i = 0; i < nparams; i++)",
|
|
"is_tuple = PyTuple_Check(item);",
|
|
"nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1;",
|
|
"argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item;",
|
|
"if (nitems != nparams)",
|
|
"nargs = PyTuple_GET_SIZE(args);",
|
|
"newargs = PyTuple_New(nargs);",
|
|
"for (iarg = 0, jarg = 0; iarg < nargs; iarg++)",
|
|
"Py_DECREF(item);",
|
|
"return newargs;",
|
|
)
|
|
positions = [subs_parameters.rfind(token) if token == "Py_DECREF(item);" else subs_parameters.find(token) for token in ordered_subs_parameters]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("_Py_subs_parameters must preserve validation, unpacking, substitution, and cleanup order")
|
|
|
|
ga_getitem = extract_function(source, "ga_getitem(PyObject *self, PyObject *item)")
|
|
if not ga_getitem.lstrip().startswith("gaobject *alias = (gaobject *)self;\n PyObject *newargs;\n PyObject *res;"):
|
|
fail("ga_getitem result locals must be declared before executable statements")
|
|
if "PyObject *newargs =" in ga_getitem or "PyObject *res =" in ga_getitem:
|
|
fail("ga_getitem must not declare initialized result locals late")
|
|
ordered_getitem = (
|
|
"if (alias->parameters == NULL)",
|
|
"alias->parameters = _Py_make_parameters(alias->args);",
|
|
"newargs = _Py_subs_parameters(self, alias->args, alias->parameters, item);",
|
|
"if (newargs == NULL)",
|
|
"res = Py_GenericAlias(alias->origin, newargs);",
|
|
"((gaobject *)res)->starred = alias->starred;",
|
|
"Py_DECREF(newargs);",
|
|
"return res;",
|
|
)
|
|
positions = [ga_getitem.find(token) for token in ordered_getitem]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_getitem must preserve lazy parameters, allocation, starred flag, and decref order")
|
|
|
|
ga_hash = extract_function(source, "ga_hash(PyObject *self)")
|
|
if "Py_hash_t h1;" not in ga_hash or "Py_hash_t h1 =" in ga_hash:
|
|
fail("ga_hash second hash local must be declared at function entry")
|
|
ordered_hash = (
|
|
"Py_hash_t h0 = PyObject_Hash(alias->origin);",
|
|
"if (h0 == -1)",
|
|
"h1 = PyObject_Hash(alias->args);",
|
|
"if (h1 == -1)",
|
|
"return h0 ^ h1;",
|
|
)
|
|
positions = [ga_hash.find(token) for token in ordered_hash]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_hash must preserve origin-first hashing and error checks")
|
|
|
|
ga_getattro = extract_function(source, "ga_getattro(PyObject *self, PyObject *name)")
|
|
if "const char * const *p;" not in ga_getattro or "for (const char * const *p =" in ga_getattro:
|
|
fail("ga_getattro attribute-table iterator must be declared before the loop")
|
|
ordered_getattro = (
|
|
"if (PyUnicode_Check(name))",
|
|
"for (p = attr_exceptions; ; p++)",
|
|
"if (*p == NULL)",
|
|
"return PyObject_GetAttr(alias->origin, name);",
|
|
"if (_PyUnicode_EqualToASCIIString(name, *p))",
|
|
"return PyObject_GenericGetAttr(self, name);",
|
|
)
|
|
positions = [ga_getattro.find(token) for token in ordered_getattro]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_getattro must preserve exception-name scan and origin fallback order")
|
|
|
|
ga_richcompare = extract_function(source, "ga_richcompare(PyObject *a, PyObject *b, int op)")
|
|
if "gaobject *aa;" not in ga_richcompare or "gaobject *bb;" not in ga_richcompare or "int eq;" not in ga_richcompare:
|
|
fail("ga_richcompare locals must be declared before execution")
|
|
if "gaobject *aa =" in ga_richcompare or "gaobject *bb =" in ga_richcompare or "int eq =" in ga_richcompare:
|
|
fail("ga_richcompare must not declare locals after control flow")
|
|
ordered_richcompare = (
|
|
"if (!_PyGenericAlias_Check(b)",
|
|
"if (op == Py_NE)",
|
|
"aa = (gaobject *)a;",
|
|
"bb = (gaobject *)b;",
|
|
"if (aa->starred != bb->starred)",
|
|
"eq = PyObject_RichCompareBool(aa->origin, bb->origin, Py_EQ);",
|
|
"if (eq < 0)",
|
|
"return PyObject_RichCompare(aa->args, bb->args, Py_EQ);",
|
|
)
|
|
positions = [ga_richcompare.find(token) for token in ordered_richcompare]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_richcompare must preserve comparison dispatch and error order")
|
|
ga_dir = extract_function(source, "ga_dir(PyObject *self, PyObject *Py_UNUSED(ignored))")
|
|
if "PyObject *dir_entry;" not in ga_dir or "const char * const *p;" not in ga_dir or "int contains;" not in ga_dir:
|
|
fail("ga_dir working locals must be declared before execution")
|
|
if "PyObject *dir_entry =" in ga_dir or "for (const char * const *p =" in ga_dir or "int contains =" in ga_dir:
|
|
fail("ga_dir must not declare locals after the origin lookup")
|
|
ordered_dir = (
|
|
"dir = PyObject_Dir(alias->origin);",
|
|
"if (dir == NULL)",
|
|
"dir_entry = NULL;",
|
|
"for (p = attr_exceptions; ; p++)",
|
|
"dir_entry = PyUnicode_FromString(*p);",
|
|
"contains = PySequence_Contains(dir, dir_entry);",
|
|
"Py_CLEAR(dir_entry);",
|
|
"return dir;",
|
|
"Py_DECREF(dir);",
|
|
"Py_XDECREF(dir_entry);",
|
|
)
|
|
positions = [ga_dir.find(token) for token in ordered_dir]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_dir must preserve directory lookup, append, and cleanup order")
|
|
|
|
ga_new = extract_function(source, "ga_new(PyTypeObject *type, PyObject *args, PyObject *kwds)")
|
|
if "PyObject *origin;" not in ga_new or "PyObject *arguments;" not in ga_new or "gaobject *self;" not in ga_new:
|
|
fail("ga_new allocation locals must be declared before validation")
|
|
if "PyObject *origin =" in ga_new or "PyObject *arguments =" in ga_new or "gaobject *self =" in ga_new:
|
|
fail("ga_new must not declare initialized locals after argument checks")
|
|
ordered_new = (
|
|
"if (!_PyArg_NoKeywords(\"GenericAlias\", kwds))",
|
|
"if (!_PyArg_CheckPositional(\"GenericAlias\", PyTuple_GET_SIZE(args), 2, 2))",
|
|
"origin = PyTuple_GET_ITEM(args, 0);",
|
|
"arguments = PyTuple_GET_ITEM(args, 1);",
|
|
"self = (gaobject *)type->tp_alloc(type, 0);",
|
|
"if (self == NULL)",
|
|
"if (!setup_ga(self, origin, arguments))",
|
|
"Py_DECREF(self);",
|
|
"return (PyObject *)self;",
|
|
)
|
|
positions = [ga_new.find(token) for token in ordered_new]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_new must preserve argument validation, allocation, and failure cleanup order")
|
|
|
|
ga_iternext = extract_function(source, "ga_iternext(gaiterobject *gi)")
|
|
if "gaobject *alias;" not in ga_iternext or "PyObject *starred_alias;" not in ga_iternext:
|
|
fail("ga_iternext result locals must be declared before the exhausted-iterator guard")
|
|
if "gaobject *alias =" in ga_iternext or "PyObject *starred_alias =" in ga_iternext:
|
|
fail("ga_iternext must not declare initialized locals after the guard")
|
|
ordered_next = (
|
|
"if (gi->obj == NULL)",
|
|
"alias = (gaobject *)gi->obj;",
|
|
"starred_alias = Py_GenericAlias(alias->origin, alias->args);",
|
|
"if (starred_alias == NULL)",
|
|
"((gaobject *)starred_alias)->starred = true;",
|
|
"Py_SETREF(gi->obj, NULL);",
|
|
"return starred_alias;",
|
|
)
|
|
positions = [ga_iternext.find(token) for token in ordered_next]
|
|
if any(position < 0 for position in positions) or positions != sorted(positions):
|
|
fail("ga_iternext must preserve exhausted-state, allocation, and ownership order")
|
|
|
|
if "#if defined(MS_WINDOWS) && defined(_MSC_VER) && _MSC_VER < 1300\nstatic PyNumberMethods ga_as_number = {0};" not in source:
|
|
fail("VC6 ga_as_number initializer must use C89-compatible zero initialization")
|
|
if "PyTypeObject _Py_GenericAliasIterType = {\n PyVarObject_HEAD_INIT(&PyType_Type, 0)\n};" not in source:
|
|
fail("VC6 GenericAlias iterator type must defer designated slots to runtime setup")
|
|
if "PyTypeObject Py_GenericAliasType = {\n PyVarObject_HEAD_INIT(&PyType_Type, 0)\n};" not in source:
|
|
fail("VC6 GenericAlias type must defer designated slots to runtime setup")
|
|
if "_Py_GenericAlias_InitTypes(void)" not in source:
|
|
fail("VC6 GenericAlias type-slot initializer missing")
|
|
init_types = extract_function(source, "_Py_GenericAlias_InitTypes(void)")
|
|
for field in ("tp_name", "tp_doc", "tp_basicsize", "tp_dealloc", "tp_repr", "tp_as_number", "tp_as_mapping", "tp_hash", "tp_call", "tp_getattro", "tp_flags", "tp_traverse", "tp_richcompare", "tp_weaklistoffset", "tp_methods", "tp_members", "tp_alloc", "tp_new", "tp_free", "tp_getset", "tp_iter", "tp_vectorcall_offset"):
|
|
if "Py_GenericAliasType." + field + " =" not in init_types:
|
|
fail("VC6 GenericAlias runtime setup missing slot: " + field)
|
|
for field in ("tp_name", "tp_basicsize", "tp_iter", "tp_iternext", "tp_traverse", "tp_methods", "tp_dealloc", "tp_clear", "tp_flags"):
|
|
if "_Py_GenericAliasIterType." + field + " =" not in init_types:
|
|
fail("VC6 GenericAlias iterator runtime setup missing slot: " + field)
|
|
if "ga_as_number.nb_or = _Py_union_type_or;" not in init_types:
|
|
fail("VC6 GenericAlias number methods runtime setup missing nb_or")
|
|
if len(sys.argv) > 2:
|
|
object_source = Path(sys.argv[2]).read_text(encoding="utf-8")
|
|
if "_Py_GenericAlias_InitTypes();" not in object_source:
|
|
fail("object.c must initialize GenericAlias type slots before PyType_Ready")
|
|
type_loop = object_source.find("// All other static types (unless initialized elsewhere)")
|
|
setup_call = object_source.find("_Py_GenericAlias_InitTypes();")
|
|
if setup_call < 0 or type_loop < 0 or setup_call > type_loop:
|
|
fail("GenericAlias type-slot initialization must precede static type readiness")
|
|
print("genericaliasobject C89 declaration and writer order: PASS")
|