278 lines
14 KiB
Python
278 lines
14 KiB
Python
#!/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")
|