#!/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")