Files
MobileGL/scripts/gen_pipe_field_ownership.py
T

773 lines
40 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*\)")
# An exemption's reason may be one string or several adjacent ones, the way a long C literal is
# written; the row regex takes the first and the check only cares that there is one.
EXEMPT_RE = re.compile(r"X\(\s*(\w+)\s*,\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"))
exempt = EXEMPT_RE.findall(macro_block(text, "MGP_VERB_OP_EXEMPT_LIST"))
if not fields:
sys.exit("gen_pipe_field_ownership: FieldOwnership.def's field list did not parse")
return fields, forwards, args, verb_ops, exempt
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, with each call's KIND) 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
calls = re.findall(r"X\(\s*(\w+)\s*,\s*\w+\s*,\s*(k\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 calls:
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 calls, [v for v, _ in verbs]
def verb_shaped_calls(calls, verbs):
"""The calls that MUST have a stamp row or an exemption, by two derived tests: the
catalogue's own kind (kCtxVerb) and a name that is also a verb's. Neither can see a call
that is a verb boundary, is not kCtxVerb and is renamed - FieldOwnership.def names the one
such call in the tree and says the phase that emits it writes its row."""
verbSet = set(verbs)
return [name for name, kind in calls if kind == "kCtxVerb" or name in verbSet]
def check_verb_ops(verb_ops, exempt, calls, verbs):
"""The stamp map, in BOTH directions.
A row whose op or verb does not exist would become a switch arm that fails to compile. An
OMITTED row is worse and is what this check exists for: the applier would run the record
under the PREVIOUS verb's serial, mask and name, so a field inside that mask reads FRESH
while holding the previous verb's value - the one failure in this package that is silent."""
opNames = [name for name, _ in calls]
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 opNames:
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)
required = verb_shaped_calls(calls, verbs)
exemptMap = {}
for op, why in exempt:
if op not in opNames:
sys.exit("gen_pipe_field_ownership: the stamp map exempts op %s, which is not a call "
"in PipeCalls.def" % op)
if op not in required:
sys.exit("gen_pipe_field_ownership: op %s is exempted from the stamp map but is not "
"verb-shaped, so it was never required - an exemption that exempts nothing "
"reads as a decision that was made" % op)
if op in seen:
sys.exit("gen_pipe_field_ownership: op %s has both a stamp row and an exemption" % op)
if not why.strip():
sys.exit("gen_pipe_field_ownership: op %s is exempted with no reason" % op)
exemptMap[op] = why
missing = [op for op in required if op not in seen and op not in exemptMap]
if missing:
sys.exit("gen_pipe_field_ownership: %d verb-shaped call(s) have no stamp row and no "
"exemption: %s - an omitted stamp point is SILENT (the record would apply under "
"the previous verb's serial, mask and name), so it is a build failure here"
% (len(missing), ", ".join(missing)))
return verb_ops, exemptMap, required
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,
exemptMap):
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.
//
// EVERY VERB-SHAPED CALL IS ANSWERED HERE OR EXEMPTED BY NAME, and the generator refuses an
// omission: a verb-shaped record with no row would apply under the PREVIOUS verb's serial,
// mask and name, so a field inside that mask would read FRESH while holding the previous
// verb's value - the one silent failure this table has. The exemptions:""")
for op in sorted(exemptMap):
add("// %-16s %s" % (op, exemptMap[op]))
add("""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("inline constexpr SizeT kMGPipeVerbBoundaryExemptCount = %d;" % len(exemptMap))
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, because, fn, quiet=False):
"""A control that must exit FOR ITS OWN REASON.
The first version of this function caught any SystemExit and asked nothing about which one,
and that is exactly how control #4 came to be a silent duplicate of control #1: its
replacement string was raw, so it mangled the row instead of blanking the phase, the row
stopped parsing, and the generator exited with "a field in NO class" while the report
counted a trip for "a BARRIER_PULLED row with no retiring phase". The guard that every debt
row names its retiring phase therefore had no control at all.
`because` is a substring the control's own message must contain. This is the discipline
gen_pipe_dirty_surface.py's self_test already uses (it asserts each control's problem
string) and it is the half that was dropped."""
try:
fn()
except SystemExit as exit:
message = str(exit.code) if exit.code is not None else ""
if because in message:
return 1
if not quiet:
print("gen_pipe_field_ownership: self-test: control %r tripped for SOMEONE ELSE'S "
"reason:\n expected to contain: %s\n actually said: %s"
% (name, because, message), file=sys.stderr)
return 0
if not quiet:
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)
calls, verbs = parse_ops_and_verbs()
def run(own_text=None, cov=None, fill_text=None, calls_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, exempt = parse_ownership(
own_text if own_text is not None else ownership_text)
c, v = parse_ops_and_verbs(calls_text) if calls_text is not None else (calls, verbs)
check_verb_ops(verb_ops, exempt, c, v)
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
# Every control below is (name, the substring its own message must contain, the edit). The
# substring is not decoration: see expect_trip.
# 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)",
"field(s) in NO class",
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)",
"GetClearColor is in TWO classes",
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",
"GetActiveTextureUnitt, which is not a PipeInputs 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".
# THE REPLACEMENT IS NOT A RAW STRING. It was, and the backslashes survived into the
# substitution, mangled the row past ROW_RE's reach and made this control a silent
# duplicate of #1 for a whole round.
unphased = edit(r"(X\(GetActiveTextureUnit," + GAP + r"BARRIER_PULLED,)" + GAP + r"\"[^\"]*\",",
"\\1 \"-\",", "blank a BARRIER_PULLED row's retiring phase")
controls.append(("a BARRIER_PULLED row with no retiring phase",
"GetActiveTextureUnit is BARRIER_PULLED and names 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,",
"\\1 SOMEHOW_FINE,", "introduce a fifth class")
controls.append(("a fifth class",
"GetActiveTextureUnit is in class SOMEHOW_FINE",
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",
"sticky forward(s) with no row: RecordError",
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",
"field row says BARRIER_PULLED and its forward row says FATAL",
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",
"narrows nothing",
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",
"EmittedCallSuppliesTheWholeField is not in PipeFill.cpp",
lambda: run(fill_text=blinded)))
# 10-11. THE STAMP MAP against both name spaces.
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",
"names op Klear, which is not a call in PipeCalls.def",
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",
"names verb DrawArrayz, which is not a verb in FillPoints.def",
lambda: run(own_text=bad_verb)))
# 12. THE OMISSION, which the first version of this gate could not see at all. A
# verb-shaped call with neither a row nor an exemption is the one silent failure in
# this package: the record applies under the PREVIOUS verb's serial, mask and name.
no_row = edit(r"X\(GenerateMipmap,\s*GenerateMipmap\)", "", "remove GenerateMipmap's stamp row")
controls.append(("a verb-shaped call with no stamp row and no exemption",
"have no stamp row and no exemption: GenerateMipmap",
lambda: run(own_text=no_row)))
# 13. An exemption is a decision, so it may not be written for a call that was never
# required - that would read as a ruling where there was none.
idle = edit(r"#define MGP_VERB_OP_EXEMPT_LIST\(X\)",
"#define MGP_VERB_OP_EXEMPT_LIST(X) X(SetDynamicState, \"not verb-shaped\") \\\n",
"exempt a call that was never required")
controls.append(("an exemption for a call that is not verb-shaped",
"SetDynamicState is exempted from the stamp map but is not verb-shaped",
lambda: run(own_text=idle)))
# 14. An exemption with no reason is an absence with extra steps.
mute = edit(r"X\(Flush," + GAP + r"\"", "X(Flush, \"\" \"", "blank an exemption's reason")
controls.append(("an exemption with no reason",
"Flush is exempted with no reason",
lambda: run(own_text=mute)))
# 15. THE REQUIRED SET'S OWN SOURCE. verb_shaped_calls reads PipeCalls.def's KIND column; if
# that column stops parsing as kCtxVerb the required set collapses to the name matches
# alone and control 12 would pass for the wrong reason. Renaming the kind is the cheapest
# way to prove the kind is actually being read.
kindless = read(PIPE_CALLS).replace("kCtxVerb", "kCtxVerbb")
controls.append(("the catalogue's kCtxVerb kind renamed away",
"is exempted from the stamp map but is not verb-shaped",
lambda: run(calls_text=kindless)))
# THE HARNESS'S OWN CONTROL, and it is the durable form of how M-1 was found. The defect was
# not the escaping in control #4; it was that expect_trip asked "did something exit" rather
# than "did THIS exit", so a control could silently become a duplicate of another. Prove the
# harness can tell them apart: control #1's edit, asserted against control #4's reason, must
# be REJECTED. Without this line the stricter harness could itself rot back.
if expect_trip("(harness control) control #1's edit under control #4's reason",
"names no retiring phase", lambda: run(own_text=dropped), quiet=True) != 0:
sys.exit("gen_pipe_field_ownership: self-test: expect_trip accepted an exit that belongs "
"to another control - the harness cannot tell one control from another, which is "
"exactly the defect that let control #4 be a silent duplicate of control #1")
trips = 0
for name, because, fn in controls:
trips += expect_trip(name, because, 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), each asserted against "
"its OWN message; harness control OK; 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, exempt = parse_ownership()
calls, verbs = parse_ops_and_verbs()
_, exemptMap, required = check_verb_ops(verb_ops, exempt, calls, 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,
exemptMap),
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)"
% (len(accessors), len(sticky), len(accessors) + len(sticky),
counts["RECORD_SUPPLIED"], counts["APPLIER_DERIVED"], counts["BARRIER_PULLED"],
counts["FATAL"], len(arg_list)))
print("gen_pipe_field_ownership: stamp map: %d verb-shaped call(s) = %d row(s) + %d "
"exemption(s), 0 unanswered"
% (len(required), len(verb_ops), len(exemptMap)))
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())