Files
MobileGL/scripts/gen_pipe_field_ownership.py
T

636 lines
31 KiB
Python

#!/usr/bin/env python3
# MobileGL - scripts/gen_pipe_field_ownership.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""TABLE 2 - where every PipeInputs field's value comes from once the backend is a server.
CONTRACT-P5.md section 3 / BRIEF-P5 R-7. Reads
MobileGL/MG_Pipe/Coverage.def the 63 accessors, the 7 sticky, the 40 emitted
MobileGL/MG_Impl/Pipe/PipeFill.cpp EmittedCallSuppliesTheWholeField's refusals
MobileGL/MG_Pipe/FieldOwnership.def the hand-maintained half
and writes MobileGL/MG_Pipe/generated/PipeFieldOwnership.inc. The output is COMMITTED; CI
regenerates it and fails on a diff, exactly as gen_pipe.py's seven outputs do.
ONE CLASS PER ROW, AND THE BUILD FAILS OTHERWISE. 63 field rows + the 7 sticky forwards =
70, each in exactly one of RECORD_SUPPLIED / APPLIER_DERIVED / BARRIER_PULLED / FATAL. A
field in none of them stops this script, so the committed header can never contain an
unclassified row and --check is what catches a stale one. RECORD_SUPPLIED is DERIVED rather
than listed - it is Coverage.def's emitted list minus PipeFill.cpp's refusals - so the
mapping file cannot drift away from the emitters it describes without a red gate.
python3 scripts/gen_pipe_field_ownership.py # write it, print the summary
python3 scripts/gen_pipe_field_ownership.py --check # THE GATE: rc 1 on any hole
python3 scripts/gen_pipe_field_ownership.py --self-test # the gate's own negative controls
"""
import argparse
import os
import re
import sys
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PIPE_DIR = os.path.join(REPO_ROOT, "MobileGL", "MG_Pipe")
GENERATED_DIR = os.path.join(PIPE_DIR, "generated")
COVERAGE_DEF = os.path.join(PIPE_DIR, "Coverage.def")
OWNERSHIP_DEF = os.path.join(PIPE_DIR, "FieldOwnership.def")
PIPE_FILL = os.path.join(REPO_ROOT, "MobileGL", "MG_Impl", "Pipe", "PipeFill.cpp")
PIPE_CALLS = os.path.join(PIPE_DIR, "PipeCalls.def")
FILL_POINTS = os.path.join(PIPE_DIR, "FillPoints.def")
OUT_NAME = "PipeFieldOwnership.inc"
CLASSES = ("RECORD_SUPPLIED", "APPLIER_DERIVED", "BARRIER_PULLED", "FATAL")
ENUMERATOR = {
"RECORD_SUPPLIED": "kRecordSupplied",
"APPLIER_DERIVED": "kApplierDerived",
"BARRIER_PULLED": "kBarrierPulled",
"FATAL": "kFatal",
}
BANNER = """// MobileGL - MobileGL/MG_Pipe/generated/{name}
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// TABLE 2: PipeInputs field ownership (CONTRACT-P5.md section 3, R-7).
//
// GENERATED by scripts/gen_pipe_field_ownership.py from Coverage.def, FieldOwnership.def and
// MG_Impl/Pipe/PipeFill.cpp - DO NOT EDIT. Regenerate with
// `python3 scripts/gen_pipe_field_ownership.py`; CI runs it and diffs the result.
//
// Included from MG_Backend/MGPipe/PipeInputs.h inside namespace MobileGL::MG_Pipe, which is
// the one header that both the poison check and the server's verb stamp already see. It is
// NOT included from MG_Pipe/MGPipe.h with the other seven generated files, on purpose: that
// header is in the PULL build's include closure and G1 admits no symbol motion there.
"""
def read(path):
with open(path, "r", encoding="utf-8") as handle:
return handle.read()
def mask_comments(text):
"""Blank comment bodies, keeping every offset and newline. String literals are LEFT
ALONE - unlike gen_pipe_dirty_surface.py's masker - because this file's rows carry their
retiring phase and their reason as quoted arguments."""
out = list(text)
i = 0
n = len(text)
while i < n:
if text[i] == "/" and i + 1 < n and text[i + 1] == "/":
while i < n and text[i] != "\n":
out[i] = " "
i += 1
elif text[i] == "/" and i + 1 < n and text[i + 1] == "*":
out[i] = out[i + 1] = " "
i += 2
while i < n and not (text[i] == "*" and i + 1 < n and text[i + 1] == "/"):
if text[i] != "\n":
out[i] = " "
i += 1
if i < n:
out[i] = out[i + 1] = " "
i += 2
else:
i += 1
return "".join(out)
def macro_block(text, name):
"""The body of a `#define <name>(X) ...` continued-line macro, comments blanked."""
masked = mask_comments(text)
start = masked.find("#define %s(X)" % name)
if start < 0:
sys.exit("gen_pipe_field_ownership: %s is not in the file" % name)
end = start
while True:
line_end = masked.find("\n", end)
if line_end < 0:
body = masked[start:]
break
if not masked[end:line_end].rstrip().endswith("\\"):
body = masked[start:line_end]
break
end = line_end + 1
# The rows wrap, so the continuation backslashes have to go before a row regex can see a
# row as one thing. Offsets do not matter here; only the token sequence does.
return body.replace("\\\n", "\n")
def parse_coverage(text=None):
"""The ordered 63 accessors, the 7 sticky and the 40 emitted, out of Coverage.def."""
text = read(COVERAGE_DEF) if text is None else text
accessors = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*\)",
macro_block(text, "MGP_COVERAGE_ACCESSOR_LIST"))
sticky = re.findall(r"X\(\s*(\w+)\s*,", macro_block(text, "MGP_COVERAGE_STICKY_LIST"))
emitted = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*\)",
macro_block(text, "MGP_COVERAGE_EMITTED_LIST"))
if not accessors:
sys.exit("gen_pipe_field_ownership: Coverage.def's accessor list did not parse")
seen = set()
for name in accessors:
if name in seen:
sys.exit("gen_pipe_field_ownership: %s appears twice in the accessor list" % name)
seen.add(name)
for name in sticky + emitted:
if name not in seen:
sys.exit("gen_pipe_field_ownership: %s is not an accessor in MGP_COVERAGE_ACCESSOR_LIST"
% name)
return accessors, sticky, emitted
def parse_supplies_whole_field(text=None):
"""The fields EmittedCallSuppliesTheWholeField REFUSES, read out of PipeFill.cpp rather
than restated here - the derivation that keeps RECORD_SUPPLIED honest is only as good as
its source, so the source is the function itself."""
text = read(PIPE_FILL) if text is None else text
masked = mask_comments(text)
start = masked.find("Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field)")
if start < 0:
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField is not in %s"
% os.path.basename(PIPE_FILL))
false_at = masked.find("return false;", start)
if false_at < 0:
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField has no `return false` arm")
refused = re.findall(r"case\s+MGPipeInputField::(\w+)\s*:", masked[start:false_at])
if not refused:
sys.exit("gen_pipe_field_ownership: EmittedCallSuppliesTheWholeField refuses nothing - "
"either the function moved or the parse broke; a silently empty refusal set "
"would make every emitted field RECORD_SUPPLIED")
return refused
ROW_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*\"([^\"]*)\"\s*,\s*\"([^\"]*)\"\s*\)")
ARG_ROW_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\d+)\s*,\s*(\w+)\s*,\s*\"([^\"]*)\"\s*\)")
PAIR_RE = re.compile(r"X\(\s*(\w+)\s*,\s*(\w+)\s*\)")
def parse_ownership(text=None):
text = read(OWNERSHIP_DEF) if text is None else text
fields = ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_LIST"))
forwards = ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_FORWARD_LIST"))
args = ARG_ROW_RE.findall(macro_block(text, "MGP_FIELD_OWNERSHIP_ARG_LIST"))
verb_ops = PAIR_RE.findall(macro_block(text, "MGP_VERB_OP_LIST"))
if not fields:
sys.exit("gen_pipe_field_ownership: FieldOwnership.def's field list did not parse")
return fields, forwards, args, verb_ops
def parse_ops_and_verbs(calls_text=None, fill_points_text=None):
"""The two name spaces the stamp map joins, read from the files that define them: the
catalogue's calls (PipeCalls.def, which IS the MGPWireOp enum) and the verb set
(FillPoints.def, which IS MG_Backend::GLFunctionsTable's member list)."""
calls_text = read(PIPE_CALLS) if calls_text is None else calls_text
fill_points_text = read(FILL_POINTS) if fill_points_text is None else fill_points_text
ops = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*,\s*\w+\s*,",
macro_block(calls_text, "MGP_CALL_LIST"))
verbs = re.findall(r"X\(\s*(\w+)\s*,\s*(k\w+)\s*\)",
macro_block(fill_points_text, "MGP_FILL_VERB_LIST"))
if not ops:
sys.exit("gen_pipe_field_ownership: PipeCalls.def's call list did not parse")
if not verbs:
sys.exit("gen_pipe_field_ownership: FillPoints.def's verb list did not parse")
return ops, [v for v, _ in verbs]
def check_verb_ops(verb_ops, ops, verbs):
"""The stamp map, against both name spaces. A row whose op or verb does not exist would
otherwise become a switch arm that fails to compile minutes later - or, worse, a silently
absent stamp point."""
if not verb_ops:
sys.exit("gen_pipe_field_ownership: MGP_VERB_OP_LIST is empty - the server would have "
"no verb boundary to stamp at and every field would read @<none>")
seen = set()
for op, verb in verb_ops:
if op not in ops:
sys.exit("gen_pipe_field_ownership: the stamp map names op %s, which is not a call "
"in PipeCalls.def" % op)
if verb not in verbs:
sys.exit("gen_pipe_field_ownership: the stamp map names verb %s, which is not a "
"verb in FillPoints.def" % verb)
if op in seen:
sys.exit("gen_pipe_field_ownership: op %s has two stamp rows" % op)
seen.add(op)
return verb_ops
def build(accessors, sticky, emitted, refused, rows, forwards, args):
"""The join, and every gate it is allowed to fail on. Returns
(ownership, phase, why, forward rows, argument rows, counts)."""
supplied = [f for f in emitted if f not in set(refused)]
ownership = {}
phase = {}
why = {}
for field in supplied:
ownership[field] = "RECORD_SUPPLIED"
phase[field] = "-"
why[field] = "derived: Coverage.def's emitted list, and PipeFill.cpp does not refuse it"
for field, cls, retires, reason in rows:
if field not in accessors:
sys.exit("gen_pipe_field_ownership: FieldOwnership.def names %s, which is not a "
"PipeInputs field" % field)
if cls not in CLASSES:
sys.exit("gen_pipe_field_ownership: %s is in class %s, which is not one of %s"
% (field, cls, "/".join(CLASSES)))
if cls == "RECORD_SUPPLIED":
sys.exit("gen_pipe_field_ownership: %s claims RECORD_SUPPLIED, which is DERIVED and "
"may not be asserted by hand" % field)
if field in ownership:
sys.exit("gen_pipe_field_ownership: %s is in TWO classes - the derivation says "
"RECORD_SUPPLIED and FieldOwnership.def says %s" % (field, cls))
if cls == "BARRIER_PULLED" and retires.strip() in ("", "-"):
sys.exit("gen_pipe_field_ownership: %s is BARRIER_PULLED and names no retiring "
"phase - P5's debt is only sized if every row says who pays it" % field)
if cls != "BARRIER_PULLED" and retires.strip() != "-":
sys.exit("gen_pipe_field_ownership: %s is %s and names a retiring phase; only "
"BARRIER_PULLED rows have one" % (field, cls))
ownership[field] = cls
phase[field] = retires
why[field] = reason
missing = [f for f in accessors if f not in ownership]
if missing:
sys.exit("gen_pipe_field_ownership: %d field(s) in NO class, which is a build failure "
"(R-7.1): %s" % (len(missing), ", ".join(missing)))
# The seven sticky forwards' own rows. They are the seven that hand the server a frontend
# object or write into the frontend, so the gate is structurally blind on them without a
# row of their own - and the row has to agree with the field row, or the two halves of the
# same accessor would be documented as different things.
forward_map = {}
for field, cls, retires, mechanism in forwards:
if field not in sticky:
sys.exit("gen_pipe_field_ownership: %s has a FORWARD row but is not sticky in "
"Coverage.def" % field)
if field in forward_map:
sys.exit("gen_pipe_field_ownership: %s has two FORWARD rows" % field)
if cls not in CLASSES:
sys.exit("gen_pipe_field_ownership: forward %s is in class %s, which is not one of "
"%s" % (field, cls, "/".join(CLASSES)))
if ownership[field] != cls:
sys.exit("gen_pipe_field_ownership: %s's field row says %s and its forward row says "
"%s; a read of a sticky field IS a call of its forward" % (field, ownership[field], cls))
forward_map[field] = (cls, retires, mechanism)
absent = [f for f in sticky if f not in forward_map]
if absent:
sys.exit("gen_pipe_field_ownership: sticky forward(s) with no row: %s" % ", ".join(absent))
arg_rows = []
for field, arg0, cls, reason in args:
if field not in accessors:
sys.exit("gen_pipe_field_ownership: argument exception names %s, which is not a "
"PipeInputs field" % field)
if cls not in CLASSES:
sys.exit("gen_pipe_field_ownership: argument exception %s(%s) is in class %s"
% (field, arg0, cls))
if cls == ownership[field]:
sys.exit("gen_pipe_field_ownership: argument exception %s(%s) repeats the field's "
"own class (%s) and narrows nothing" % (field, arg0, cls))
arg_rows.append((field, int(arg0), cls, reason))
counts = {cls: sum(1 for f in accessors if ownership[f] == cls) for cls in CLASSES}
return ownership, phase, why, forward_map, arg_rows, counts
def emit(accessors, sticky, ownership, phase, why, forward_map, arg_rows, counts, verb_ops):
out = [BANNER.format(name=OUT_NAME)]
add = out.append
add("""
// The four classes. kUnclassified exists so the static_assert below has something to refuse;
// the generator never emits it, which is what makes "a field in no class fails the build"
// true at two independent points rather than one.
enum class MGPipeFieldOwnership : Uint8 {
kUnclassified = 0,
kRecordSupplied, // a pushed record supplies the WHOLE field
kApplierDerived, // the applier writes it out of records it already applies
kBarrierPulled, // P5's debt: read out of the client's residual fill under the verb barrier
kFatal, // no carrier and the reduced path never reads it
};
inline constexpr const char* kMGPipeFieldOwnershipNames[] = {
"UNCLASSIFIED", "RECORD-SUPPLIED", "APPLIER-DERIVED", "BARRIER-PULLED", "FATAL",
};
""")
add("inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnership[kMGPipeInputFieldCount] = {")
for field in accessors:
add(" MGPipeFieldOwnership::%s, // %s" % (ENUMERATOR[ownership[field]], field))
add("};\n")
add("// The ROADMAP phase whose row retires the pull. \"-\" for every class but BARRIER-PULLED.")
add("inline constexpr const char* kMGPipeFieldRetiringPhase[kMGPipeInputFieldCount] = {")
for field in accessors:
add(" \"%s\", // %s" % (phase[field], field))
add("};\n")
add("// The seven sticky forwards, which are among the 63 above and need a row of their own:")
add("// they are the ones that hand the server a raw frontend object or write into the")
add("// frontend, so the exit gate is structurally blind on them without one.")
add("inline constexpr SizeT kMGPipeFieldOwnershipForwardCount = %d;" % len(sticky))
add("static_assert(kMGPipeFieldOwnershipForwardCount == kMGPipeInputStickyFieldCount,")
add(" \"the forward rows and Coverage.def's sticky set are the same seven\");")
add("inline constexpr MGPipeInputField kMGPipeFieldOwnershipForwardField[kMGPipeFieldOwnershipForwardCount] = {")
for field in sticky:
add(" MGPipeInputField::%s," % field)
add("};")
add("inline constexpr MGPipeFieldOwnership kMGPipeFieldOwnershipForward[kMGPipeFieldOwnershipForwardCount] = {")
for field in sticky:
add(" MGPipeFieldOwnership::%s, // %s" % (ENUMERATOR[forward_map[field][0]], field))
add("};")
add("inline constexpr const char* kMGPipeFieldOwnershipForwardMechanism[kMGPipeFieldOwnershipForwardCount] = {")
for field in sticky:
add(" \"%s\", // %s, retires in %s" % (forward_map[field][2], field, forward_map[field][1]))
add("};\n")
add("// CONTRACT-P5.md section 3: \"70 rows, each in exactly one class\".")
add("inline constexpr SizeT kMGPipeFieldOwnershipRowCount =")
add(" kMGPipeInputFieldCount + kMGPipeFieldOwnershipForwardCount;")
add("static_assert(kMGPipeFieldOwnershipRowCount == 70, \"table 2's row count moved\");\n")
add("""// An ARGUMENT-KEYED narrowing of one field. The field keeps its single row above; this
// says that one argument value of it belongs to a different class. Coverage.def:62-69 already
// rules the shape for GetBufferBindingSlot - "THE ROW STAYS ONE ROW ... the field is ONE array
// that a second row of the same name could only duplicate" - and m_pixelStore[2] is the same
// shape indexed by its own isUnpack argument.
struct MGPipeFieldArgumentOwnership {
MGPipeInputField Field;
Uint32 Arg0;
MGPipeFieldOwnership Class;
};""")
add("inline constexpr SizeT kMGPipeFieldArgumentOwnershipCount = %d;" % len(arg_rows))
add("inline constexpr MGPipeFieldArgumentOwnership")
add(" kMGPipeFieldArgumentOwnership[kMGPipeFieldArgumentOwnershipCount] = {")
for field, arg0, cls, reason in arg_rows:
add(" {MGPipeInputField::%s, %du, MGPipeFieldOwnership::%s}, // %s"
% (field, arg0, ENUMERATOR[cls], reason))
add("};\n")
add("""constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field) {
return kMGPipeFieldOwnership[static_cast<SizeT>(field)];
}
// The same answer, narrowed by the accessor's first argument. Every accessor that takes one
// may call this; only the fields with a row above answer differently from the field's class.
constexpr MGPipeFieldOwnership MGPipeFieldOwnershipOf(MGPipeInputField field, Uint32 arg0) {
for (SizeT i = 0; i < kMGPipeFieldArgumentOwnershipCount; ++i) {
if (kMGPipeFieldArgumentOwnership[i].Field == field &&
kMGPipeFieldArgumentOwnership[i].Arg0 == arg0) {
return kMGPipeFieldArgumentOwnership[i].Class;
}
}
return MGPipeFieldOwnershipOf(field);
}
constexpr const char* MGPipeFieldOwnershipName(MGPipeFieldOwnership ownership) {
return kMGPipeFieldOwnershipNames[static_cast<SizeT>(ownership)];
}
// THE BUILD FAILURE R-7.1 ASKS FOR. The generator refuses to emit an unclassified row, so
// this can only fire on a hand-edited header - which is exactly the edit the DO NOT EDIT
// banner cannot prevent on its own.
constexpr Bool MGPipeEveryFieldIsClassified() {
for (SizeT i = 0; i < kMGPipeInputFieldCount; ++i) {
if (kMGPipeFieldOwnership[i] == MGPipeFieldOwnership::kUnclassified) return false;
}
for (SizeT i = 0; i < kMGPipeFieldOwnershipForwardCount; ++i) {
if (kMGPipeFieldOwnershipForward[i] == MGPipeFieldOwnership::kUnclassified) return false;
}
return true;
}
static_assert(MGPipeEveryFieldIsClassified(),
"a PipeInputs field is in none of the four ownership classes (CONTRACT-P5 table 2, R-7.1)");
""")
add("""// WHERE THE SERVER STAMPS. The wire's op and the fill's verb are different name spaces
// and do not line up by name (draw_vbo is DrawArrays, blit is BlitFramebuffer), so this is the
// join. An op with no row is NOT a verb boundary and the applier must not stamp on it.
// Present is deliberately absent: FillPoints.def:21 - "Present and SetSwapInterval go through
// BackendObject virtuals and read no frontend state, so they are not verbs here".
constexpr MGPipeVerb MGPipeVerbForWireOp(MGPWireOp op) {
switch (op) {""")
for op, verb in verb_ops:
add(" case MGPWireOp::%s: return MGPipeVerb::%s;" % (op, verb))
add(""" default:
return MGPipeVerb::kVerbCount;
}
}
""")
add("inline constexpr SizeT kMGPipeVerbBoundaryOpCount = %d;" % len(verb_ops))
add("")
add("// The class sizes, as constants a test can pin without recounting the table.")
for cls in CLASSES:
add("inline constexpr SizeT kMGPipe%sFieldCount = %d;"
% ("".join(p.capitalize() for p in cls.split("_")), counts[cls]))
add("static_assert(%s == kMGPipeInputFieldCount, \"the four class sizes do not partition the field set\");"
% " + ".join("kMGPipe%sFieldCount" % "".join(p.capitalize() for p in cls.split("_"))
for cls in CLASSES))
return "\n".join(out) + "\n"
def write(path, text, check_only, changed):
existing = read(path) if os.path.exists(path) else None
if existing == text:
return
changed.append(os.path.basename(path))
if not check_only:
with open(path, "w", encoding="utf-8", newline="\n") as handle:
handle.write(text)
def expect_trip(name, fn):
"""A control that must exit. Returns 1 when it did, and says so when it did not - a
silent pass here is the gate not checking anything."""
try:
fn()
except SystemExit:
return 1
print("gen_pipe_field_ownership: self-test: control did NOT trip: %s" % name, file=sys.stderr)
return 0
def self_test():
"""The negative controls (gen_pipe.py --self-test's shape): each gate must go red for its
own reason, and zero trips is itself an error."""
coverage = read(COVERAGE_DEF)
ownership_text = read(OWNERSHIP_DEF)
fill = read(PIPE_FILL)
accessors, sticky, emitted = parse_coverage(coverage)
refused = parse_supplies_whole_field(fill)
ops, verbs = parse_ops_and_verbs()
def run(own_text=None, cov=None, fill_text=None):
acc, stk, emt = parse_coverage(cov if cov is not None else coverage)
ref = parse_supplies_whole_field(fill_text if fill_text is not None else fill)
rows, fwd, args, verb_ops = parse_ownership(own_text if own_text is not None else ownership_text)
check_verb_ops(verb_ops, ops, verbs)
return build(acc, stk, emt, ref, rows, fwd, args)
# The rows wrap over two lines with a trailing backslash, so every control below edits
# them through a regex whose gaps tolerate that rather than through a literal that would
# silently stop matching the day someone re-aligns the file.
GAP = r"[\s\\]*"
def edit(pattern, replacement, what, text=None):
text = ownership_text if text is None else text
edited, count = re.subn(pattern, replacement, text, count=1)
if count != 1:
sys.exit("gen_pipe_field_ownership: self-test: could not %s - the control's own "
"edit no longer matches the file it is supposed to break" % what)
return edited
# 1. THE HEADLINE CONTROL (exit gate E4): take one field out of the table. It is in no
# class, and that is a build failure rather than a silent default.
dropped = edit(r"X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED," + GAP + r"\"[^\"]*\","
+ GAP + r"\"[^\"]*\"\)", "", "remove GetActiveTextureUnit's row")
controls = [("a field in NO class (GetActiveTextureUnit's row removed)",
lambda: run(own_text=dropped))]
# 2. The other direction: a field the derivation already placed in RECORD_SUPPLIED, also
# claimed by hand. E4's negative control is "move a field from supplied to FATAL".
doubled = ownership_text.replace(
"#define MGP_FIELD_OWNERSHIP_LIST(X)",
"#define MGP_FIELD_OWNERSHIP_LIST(X) X(GetClearColor, FATAL, \"-\", \"moved by hand\") \\\n", 1)
controls.append(("a RECORD_SUPPLIED field claimed by hand (supplied -> FATAL)",
lambda: run(own_text=doubled)))
# 3. A row naming something that is not a field at all.
typo = edit(r"X\(GetActiveTextureUnit,", "X(GetActiveTextureUnitt,", "misspell a field name")
controls.append(("a row naming a non-field", lambda: run(own_text=typo)))
# 4. A BARRIER_PULLED row with no retiring phase: the debt is only sized if every row
# says who pays it, which is the whole of R-7.2's "rsp IS the size of the debt".
unphased = edit(r"(X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED,)" + GAP + r"\"[^\"]*\",",
r"\1 \"-\",", "blank a BARRIER_PULLED row's retiring phase")
controls.append(("a BARRIER_PULLED row with no retiring phase", lambda: run(own_text=unphased)))
# 5. A class that is not one of the four.
bogus = edit(r"(X\(GetActiveTextureUnit,)" + GAP + r"BARRIER_PULLED,",
r"\1 SOMEHOW_FINE,", "introduce a fifth class")
controls.append(("a fifth class", lambda: run(own_text=bogus)))
# 6. A sticky forward that lost its own row - the seven most dangerous fields are exactly
# the ones a table without forward rows is blind to.
no_forward = edit(r"X\(RecordError," + GAP + r"BARRIER_PULLED," + GAP + r"\"P9\"," + GAP
+ r"\"OnGlError[^\"]*\"\)", "", "remove RecordError's forward row")
controls.append(("a sticky forward with no row", lambda: run(own_text=no_forward)))
# 7. A forward row that disagrees with its field row.
disagree = edit(r"X\(GetTextureObject," + GAP + r"BARRIER_PULLED," + GAP + r"\"P7\"," + GAP
+ r"\"a server-side texture handle table\"\)",
"X(GetTextureObject, FATAL, \"-\", \"a server-side texture handle table\")",
"contradict GetTextureObject's field row")
controls.append(("a forward row that contradicts its field row", lambda: run(own_text=disagree)))
# 8. An argument exception that narrows nothing.
same = edit(r"X\(GetPixelStoreParameters, 1, FATAL,",
"X(GetPixelStoreParameters, 1, APPLIER_DERIVED,",
"make the argument exception repeat the field's class")
controls.append(("an argument exception that repeats the field's class",
lambda: run(own_text=same)))
# 9. THE DERIVATION'S OWN SOURCE. If EmittedCallSuppliesTheWholeField's refusals stop
# parsing, every emitted field silently becomes RECORD_SUPPLIED and eight rows of this
# table quietly contradict themselves - so an empty refusal set has to stop the script
# rather than produce a plausible table.
blinded = fill.replace("Bool EmittedCallSuppliesTheWholeField(MGPipeInputField field)",
"Bool EmittedCallSuppliesTheWholeFieldXX(MGPipeInputField field)", 1)
controls.append(("the derivation's source function renamed away",
lambda: run(fill_text=blinded)))
# 10. THE STAMP MAP against both name spaces. A row naming an op that is not a call, or a
# verb that is not a verb, would otherwise be an arm that fails to compile - or an
# absent stamp point, which is silent.
bad_op = edit(r"X\(Clear,\s*Clear\)", "X(Klear, Clear)", "misspell a stamp map op")
controls.append(("a stamp row naming an op that does not exist", lambda: run(own_text=bad_op)))
bad_verb = edit(r"X\(DrawVbo,\s*DrawArrays\)", "X(DrawVbo, DrawArrayz)",
"misspell a stamp map verb")
controls.append(("a stamp row naming a verb that does not exist", lambda: run(own_text=bad_verb)))
trips = 0
for name, fn in controls:
trips += expect_trip(name, fn)
# The positive control: the real tables pass, and they partition the real field set.
_, _, _, _, _, counts = run()
total = sum(counts.values())
if total != len(accessors):
sys.exit("gen_pipe_field_ownership: self-test: the positive control does not partition "
"the field set (%d of %d)" % (total, len(accessors)))
if len(sticky) != 7:
sys.exit("gen_pipe_field_ownership: self-test: Coverage.def no longer has seven sticky fields")
if len(refused) != 9:
sys.exit("gen_pipe_field_ownership: self-test: EmittedCallSuppliesTheWholeField refuses %d "
"fields, not the nine the contract's derivation is written against" % len(refused))
if trips == 0:
sys.exit("gen_pipe_field_ownership: self-test: no negative control tripped - the gates are "
"not checking anything")
if trips != len(controls):
sys.exit("gen_pipe_field_ownership: self-test: %d of %d negative controls did not trip"
% (len(controls) - trips, len(controls)))
print("gen_pipe_field_ownership: self-test: %d negative-control trip(s), positive control OK "
"(%d fields partitioned, 7 sticky forwards, 9 refusals)" % (trips, total))
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true",
help="do not write; exit 1 if regenerating would change anything")
parser.add_argument("--self-test", action="store_true",
help="run the negative controls (each gate must trip) and exit")
args = parser.parse_args()
if args.self_test:
return self_test()
accessors, sticky, emitted = parse_coverage()
refused = parse_supplies_whole_field()
rows, forwards, arg_rows, verb_ops = parse_ownership()
ops, verbs = parse_ops_and_verbs()
check_verb_ops(verb_ops, ops, verbs)
ownership, phase, why, forward_map, arg_list, counts = build(
accessors, sticky, emitted, refused, rows, forwards, arg_rows)
if not os.path.isdir(GENERATED_DIR):
os.makedirs(GENERATED_DIR)
changed = []
write(os.path.join(GENERATED_DIR, OUT_NAME),
emit(accessors, sticky, ownership, phase, why, forward_map, arg_list, counts, verb_ops),
args.check, changed)
print("gen_pipe_field_ownership: %d fields + %d sticky forwards = %d rows; "
"%d record-supplied (derived), %d applier-derived, %d barrier-pulled, %d fatal, "
"%d argument exception(s), %d verb-boundary op(s)"
% (len(accessors), len(sticky), len(accessors) + len(sticky),
counts["RECORD_SUPPLIED"], counts["APPLIER_DERIVED"], counts["BARRIER_PULLED"],
counts["FATAL"], len(arg_list), len(verb_ops)))
pulled = [(f, phase[f]) for f in accessors if ownership[f] == "BARRIER_PULLED"]
print("gen_pipe_field_ownership: the debt, by retiring phase:")
by_phase = {}
for field, retires in pulled:
by_phase.setdefault(retires, []).append(field)
for retires in sorted(by_phase):
print("gen_pipe_field_ownership: %-42s %d" % (retires, len(by_phase[retires])))
if changed:
if args.check:
print("gen_pipe_field_ownership: OUT OF DATE: %s" % ", ".join(changed), file=sys.stderr)
return 1
print("gen_pipe_field_ownership: wrote %s" % ", ".join(changed))
else:
print("gen_pipe_field_ownership: generated file is up to date")
return 0
if __name__ == "__main__":
sys.exit(main())