#!/usr/bin/env python3 """Guard C89 declarations and frame traversal order in genobject.c.""" 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") body = extract_function(source, "gen_traverse(PyGenObject *gen, visitproc visit, void *arg)") if "int err;" not in body or "int err =" in body: fail("gen_traverse frame result local must be declared before traversal assertions") ordered = ( "if (gen->gi_frame_state < FRAME_CLEARED)", "_PyInterpreterFrame *frame = (_PyInterpreterFrame *)(gen->gi_iframe);", "assert(frame->frame_obj == NULL ||", "err = _PyFrame_Traverse(frame, visit, arg);", "if (err)", "return exc_state_traverse(&gen->gi_exc_state, visit, arg);", ) positions = [body.find(token) for token in ordered] if any(position < 0 for position in positions) or positions != sorted(positions): fail("gen_traverse must preserve frame validation, visit, and error return order") gen_yf = extract_function(source, "_PyGen_yf(PyGenObject *gen)") if "_Py_CODEUNIT next;" not in gen_yf or "_Py_CODEUNIT next =" in gen_yf: fail("_PyGen_yf instruction local must be declared at function entry") ordered_yf = ( "PyObject *yf = NULL;", "if (gen->gi_frame_state < FRAME_CLEARED)", "if (gen->gi_frame_state == FRAME_CREATED)", "return NULL;", "next = frame->prev_instr[1];", "if (_PyOpcode_Deopt[_Py_OPCODE(next)] != RESUME", "yf = _PyFrame_StackPeek(frame);", "Py_INCREF(yf);", "return yf;", ) positions = [gen_yf.find(token) for token in ordered_yf] if any(position < 0 for position in positions) or positions != sorted(positions): fail("_PyGen_yf must preserve created-frame guard and yield-from inspection order") gen_throw = extract_function(source, "_gen_throw(PyGenObject *gen, int close_on_genexit,\n PyObject *typ, PyObject *val, PyObject *tb)") if "PyFrameState state;" not in gen_throw: fail("_gen_throw nested generator-state locals must be declared at branch entry") recursive_branch = gen_throw.split("if (PyGen_CheckExact(yf) || PyCoro_CheckExact(yf)) {", 1)[1].split("} else {", 1)[0] if "PyFrameState state;" not in recursive_branch or "PyFrameState state =" in recursive_branch: fail("_gen_throw recursive branch state local must be declared before current-frame updates") ordered_recursive = ( "PyThreadState *tstate = _PyThreadState_GET();", "_PyInterpreterFrame *prev = tstate->cframe->current_frame;", "frame->previous = prev;", "tstate->cframe->current_frame = frame;", "state = gen->gi_frame_state;", "gen->gi_frame_state = FRAME_EXECUTING;", "ret = _gen_throw((PyGenObject *)yf, close_on_genexit,", "gen->gi_frame_state = state;", "tstate->cframe->current_frame = prev;", "frame->previous = NULL;", ) positions = [recursive_branch.find(token) for token in ordered_recursive] if any(position < 0 for position in positions) or positions != sorted(positions): fail("_gen_throw must preserve recursive frame/state restoration order") iterator_branch = gen_throw.split("} else {", 1)[1] if "PyFrameState state;" not in iterator_branch or "PyFrameState state =" in iterator_branch: fail("_gen_throw iterator branch state local must be declared at branch entry") ordered_iterator = ( "PyObject *meth;", "if (_PyObject_LookupAttr(yf, &_Py_ID(throw), &meth) < 0)", "if (meth == NULL)", "state = gen->gi_frame_state;", "gen->gi_frame_state = FRAME_EXECUTING;", "ret = PyObject_CallFunctionObjArgs(meth, typ, val, tb, NULL);", "gen->gi_frame_state = state;", ) positions = [iterator_branch.find(token) for token in ordered_iterator] if any(position < 0 for position in positions) or positions != sorted(positions): fail("_gen_throw must preserve iterator lookup and state restoration order") throw_failure = gen_throw.split("if (!ret) {", 1)[1] if "int jump;" not in throw_failure or "int jump =" in throw_failure: fail("_gen_throw SEND jump local must be declared at its failure-branch entry") ordered_jump = ( "PyObject *val;", "assert(gen->gi_frame_state < FRAME_CLEARED);", "ret = _PyFrame_StackPop((_PyInterpreterFrame *)gen->gi_iframe);", "assert(_PyInterpreterFrame_LASTI(frame) >= 0);", "assert(_Py_OPCODE(frame->prev_instr[-1]) == SEND);", "jump = _Py_OPARG(frame->prev_instr[-1]);", "frame->prev_instr += jump - 1;", "if (_PyGen_FetchStopIterationValue(&val) == 0)", ) positions = [throw_failure.find(token) for token in ordered_jump] if any(position < 0 for position in positions) or positions != sorted(positions): fail("_gen_throw must preserve SEND validation and jump/stop-value handling order") gen_sizeof = extract_function(source, "gen_sizeof(PyGenObject *gen, PyObject *Py_UNUSED(ignored))") if "PyCodeObject *code;" not in gen_sizeof or "PyCodeObject *code =" in gen_sizeof: fail("gen_sizeof code local must be declared before size calculation") ordered_sizeof = ( "res = offsetof(PyGenObject, gi_iframe) + offsetof(_PyInterpreterFrame, localsplus);", "code = gen->gi_code;", "res += (code->co_nlocalsplus+code->co_stacksize) * sizeof(PyObject *);", "return PyLong_FromSsize_t(res);", ) positions = [gen_sizeof.find(token) for token in ordered_sizeof] if any(position < 0 for position in positions) or positions != sorted(positions): fail("gen_sizeof must preserve base-size, code-size, and result order") make_coro = extract_function(source, "_Py_MakeCoro(PyFunctionObject *func)") entry_coro = ("int coro_flags;", "PyObject *coro;", "PyThreadState *tstate;", "int origin_depth;") remaining = make_coro.lstrip() for declaration in entry_coro: if not remaining.startswith(declaration + "\n"): fail("_Py_MakeCoro entry declaration missing or out of order: " + declaration) remaining = remaining[len(declaration):].lstrip() if "int coro_flags =" in make_coro or "PyObject *coro =" in make_coro or "PyThreadState *tstate =" in make_coro or "int origin_depth =" in make_coro: fail("_Py_MakeCoro must not declare initialized locals after control flow") if "PyObject *cr_origin;" not in make_coro or "PyObject *cr_origin =" in make_coro: fail("_Py_MakeCoro origin local must be declared at the fallback branch entry") ordered_coro = ( "coro_flags = ((PyCodeObject *)func->func_code)->co_flags &", "if (coro_flags == CO_GENERATOR)", "if (coro_flags == CO_ASYNC_GENERATOR)", "assert (coro_flags == CO_COROUTINE);", "coro = make_gen(&PyCoro_Type, func);", "tstate = _PyThreadState_GET();", "origin_depth = tstate->coroutine_origin_tracking_depth;", "if (origin_depth == 0)", "cr_origin = compute_cr_origin(origin_depth, _PyEval_GetFrame()->previous);", "return coro;", ) positions = [make_coro.find(token) for token in ordered_coro] if any(position < 0 for position in positions) or positions != sorted(positions): fail("_Py_MakeCoro must preserve coroutine selection, creation, and origin tracking order") compute_origin_start = source.rfind("static PyObject *\ncompute_cr_origin(int origin_depth, _PyInterpreterFrame *current_frame)") if compute_origin_start < 0: fail("missing compute_cr_origin definition") compute_origin = extract_function(source[compute_origin_start:], "compute_cr_origin(int origin_depth, _PyInterpreterFrame *current_frame)") for declaration in ("_PyInterpreterFrame *frame;", "int frame_count;", "PyObject *cr_origin;", "int i;"): if declaration not in compute_origin: fail("compute_cr_origin local must be declared before frame traversal: " + declaration) if "int frame_count =" in compute_origin or "PyObject *cr_origin =" in compute_origin or "for (int i =" in compute_origin: fail("compute_cr_origin must not declare initialized locals after traversal starts") ordered_origin = ( "frame = current_frame;", "frame_count = 0;", "for (; frame && frame_count < origin_depth; ++frame_count)", "cr_origin = PyTuple_New(frame_count);", "if (cr_origin == NULL)", "frame = current_frame;\n for (i = 0; i < frame_count; ++i)", "PyTuple_SET_ITEM(cr_origin, i, frameinfo);", "return cr_origin;", ) positions = [compute_origin.find(token) for token in ordered_origin] if any(position < 0 for position in positions) or positions != sorted(positions): fail("compute_cr_origin must preserve frame counting, tuple creation, and collection order") pycoro_new = extract_function(source, "PyCoro_New(PyFrameObject *f, PyObject *name, PyObject *qualname)") for declaration in ("PyObject *coro;", "PyThreadState *tstate;", "int origin_depth;"): if declaration not in pycoro_new: fail("PyCoro_New local must be declared at function entry: " + declaration) if "PyObject *coro =" in pycoro_new or "PyThreadState *tstate =" in pycoro_new or "int origin_depth =" in pycoro_new: fail("PyCoro_New must not declare initialized locals after allocation") if "PyObject *cr_origin;" not in pycoro_new or "PyObject *cr_origin =" in pycoro_new: fail("PyCoro_New origin local must be declared at the fallback branch entry") ordered_pycoro = ( "coro = gen_new_with_qualname(&PyCoro_Type, f, name, qualname);", "if (!coro)", "tstate = _PyThreadState_GET();", "origin_depth = tstate->coroutine_origin_tracking_depth;", "if (origin_depth == 0)", "cr_origin = compute_cr_origin(origin_depth, _PyEval_GetFrame());", "return coro;", ) positions = [pycoro_new.find(token) for token in ordered_pycoro] if any(position < 0 for position in positions) or positions != sorted(positions): fail("PyCoro_New must preserve allocation and origin-tracking order") gen_new_qualname = extract_function(source, "gen_new_with_qualname(PyTypeObject *type, PyFrameObject *f,\n PyObject *name, PyObject *qualname)") if "_PyInterpreterFrame *frame;" not in gen_new_qualname or "_PyInterpreterFrame *frame =" in gen_new_qualname: fail("gen_new_with_qualname frame local must be declared before allocation checks") ordered_gen_new = ( "PyCodeObject *code = f->f_frame->f_code;", "int size = code->co_nlocalsplus + code->co_stacksize;", "PyGenObject *gen = PyObject_GC_NewVar(PyGenObject, type, size);", "if (gen == NULL)", "assert(f->f_frame->frame_obj == NULL);", "assert(f->f_frame->owner == FRAME_OWNED_BY_FRAME_OBJECT);", "frame = (_PyInterpreterFrame *)gen->gi_iframe;", "_PyFrame_Copy((_PyInterpreterFrame *)f->_f_frame_data, frame);", "gen->gi_frame_state = FRAME_CREATED;", "return (PyObject *)gen;", ) positions = [gen_new_qualname.find(token) for token in ordered_gen_new] if any(position < 0 for position in positions) or positions != sorted(positions): fail("gen_new_with_qualname must preserve frame validation, copy, and ownership order") asend_dealloc = extract_function(source, "async_gen_asend_dealloc(PyAsyncGenASend *o)") if not asend_dealloc.lstrip().startswith("#if _PyAsyncGen_MAXFREELIST > 0\n struct _Py_async_gen_state *state;\n#endif"): fail("async_gen_asend_dealloc state local must be declared at function entry under its feature guard") if "struct _Py_async_gen_state *state =" in asend_dealloc: fail("async_gen_asend_dealloc must not declare state after object cleanup") ordered_asend_dealloc = ( "_PyObject_GC_UNTRACK((PyObject *)o);", "Py_CLEAR(o->ags_gen);", "Py_CLEAR(o->ags_sendval);", "state = get_async_gen_state();", "assert(state->asend_numfree != -1);", "if (state->asend_numfree < _PyAsyncGen_MAXFREELIST)", "state->asend_freelist[state->asend_numfree++] = o;", "PyObject_GC_Del(o);", ) positions = [asend_dealloc.find(token) for token in ordered_asend_dealloc] if any(position < 0 for position in positions) or positions != sorted(positions): fail("async_gen_asend_dealloc must preserve untracking, clearing, freelist, and free order") wrapped_dealloc = extract_function(source, "async_gen_wrapped_val_dealloc(_PyAsyncGenWrappedValue *o)") if not wrapped_dealloc.lstrip().startswith("#if _PyAsyncGen_MAXFREELIST > 0\n struct _Py_async_gen_state *state;\n#endif"): fail("wrapped-value freelist state must be declared at function entry under its feature guard") ordered_wrapped = ( "_PyObject_GC_UNTRACK((PyObject *)o);", "Py_CLEAR(o->agw_val);", "state = get_async_gen_state();", "assert(state->value_numfree != -1);", "if (state->value_numfree < _PyAsyncGen_MAXFREELIST)", "state->value_freelist[state->value_numfree++] = o;", "PyObject_GC_Del(o);", ) positions = [wrapped_dealloc.find(token) for token in ordered_wrapped] if any(position < 0 for position in positions) or positions != sorted(positions): fail("wrapped-value deallocator must preserve untrack, clear, freelist, and free order") wrapped_new = extract_function(source, "_PyAsyncGenValueWrapperNew(PyObject *val)") state_decl = "#if _PyAsyncGen_MAXFREELIST > 0\n struct _Py_async_gen_state *state;\n#endif" if state_decl not in wrapped_new or wrapped_new.find(state_decl) > wrapped_new.find("assert(val);"): fail("_PyAsyncGenValueWrapperNew freelist state must be declared before executable statements") if "struct _Py_async_gen_state *state =" in wrapped_new: fail("_PyAsyncGenValueWrapperNew must not initialize state in a late declaration") ordered_wrapped_new = ( "assert(val);", "state = get_async_gen_state();", "assert(state->value_numfree != -1);", "if (state->value_numfree)", "o = PyObject_GC_New(_PyAsyncGenWrappedValue,", "o->agw_val = val;", "Py_INCREF(val);", "_PyObject_GC_TRACK((PyObject*)o);", "return (PyObject*)o;", ) positions = [wrapped_new.find(token) for token in ordered_wrapped_new] if any(position < 0 for position in positions) or positions != sorted(positions): fail("_PyAsyncGenValueWrapperNew must preserve freelist, allocation, and value ownership order") print("genobject C89 declaration and generator ownership order: PASS")