pipe/test/fixtures/cpp_state/src/engine.cpp
trvon 4b499c1e21
feat(cpp): experimental C++ front end for scoped-enum state
tool/cpp_front.py is a second front end written against the state model,
not the rules: it reads a compile database with libclang (Homebrew LLVM)
and writes the JSON that `pipe check --model` takes. Every rule,
baseline and gate works on it unchanged.

Mapping, per project `enum class` (identity from clang's resolved AST,
so two enums both named QueryIntent are never confused):
- a reference that produces a value (assignment, atomic store, argument,
  return, initialiser) is an emission; a cast from an int is an indirect
  one;
- a `case`, `==` or `!=` is an observation;
- a busy enumerator written to the method's OWN state, followed by a
  top-level try whose catch neither writes state nor rethrows, is a
  swallowed failure;
- sources the build does not compile are recorded as unresolved, so
  findings naming them drop a confidence step.

One rule-side change, optional and Dart-neutral: an EmissionSite may name
its stateType. rendered-not-emitted attributed indirect emissions by owner
(a bloc owns one state type), so a cast to one enum made "never emitted"
unprovable for every other enum in the same C++ class. With stateType
set, the suppression is scoped to that enum. Dart output is still byte
identical on the fixture and all four dogfood apps.

Run on ~/work/tools/yams (375 translation units from build/debug, 0 clang
errors, about two minutes): 118 state enums, 82 findings. A grep-based
hand check of 13 found 12 factually correct - e.g. the IPC connection FSM
never enters ImplState::ReadingHeader, the downloader never reports
ProgressStage::Verifying though three CLI commands render it,
ExtractScopeType qualifiers are parsed but never acted on - and one false
positive (a nested JSON-parse fallback taken for a stuck busy state),
now excluded by requiring the method's own state and a top-level try.

Bugs found on the way and fixed: ccache/compiler wrappers passed as
inputs, relative -I paths resolved against the wrong directory (7 TUs
silently lost third_party headers), a symlinked path putting every TU
"outside" the project, and an empty compile database producing an empty
"clean" model with exit 0 - all now fail loudly or are handled.

test/cpp_front_test.dart runs the front end on test/fixtures/cpp_state
with positive and negative controls for each rule; skipped without LLVM.
138 tests pass.
2026-09-22 19:16:53 -06:00

104 lines
2.3 KiB
C++

// Fixture for tool/cpp_front.py: each case is a positive or negative control
// for a rule run on the C++ state model (see test/cpp_front_test.dart).
namespace fx {
// Ghost is compared against but never produced: rendered-not-emitted.
enum class Phase { Idle, Starting, Running, Failed, Ghost };
// Produced, never compared or switched on: emitted-not-rendered.
enum class Unread { A, B };
// Y is compared against and produced only through a cast from an int, so the
// rule must suppress itself rather than call the branch dead.
enum class Casted { X, Y };
void mayThrow();
class Engine {
public:
// Negative: the catch resolves the busy state.
void startGuarded() {
phase_ = Phase::Starting;
try {
mayThrow();
phase_ = Phase::Running;
} catch (...) {
phase_ = Phase::Failed;
}
}
// Positive: busy state set, then a top-level try whose catch swallows the
// failure, so Starting can outlive it (loading-not-reset).
void startSwallowed() {
phase_ = Phase::Starting;
try {
mayThrow();
phase_ = Phase::Running;
} catch (...) {
}
}
// Negative: a nested try is a local fallback, not the method's failure path.
void startNested() {
phase_ = Phase::Starting;
if (ready_) {
try {
mayThrow();
} catch (...) {
}
}
phase_ = Phase::Running;
}
bool ghost() const { return phase_ == Phase::Ghost; }
bool idle() const {
switch (phase_) {
case Phase::Idle:
return true;
case Phase::Starting:
case Phase::Running:
case Phase::Failed:
return false;
default:
return false;
}
}
void setUnread() {
unread_ = Unread::A;
unread_ = Unread::B;
}
void load(int n) { casted_ = static_cast<Casted>(n); }
bool isY() const { return casted_ == Casted::Y; }
private:
bool ready_ = false;
Phase phase_ = Phase::Idle;
Unread unread_ = Unread::A;
Casted casted_ = Casted::X;
};
// Negative: `ev.stage` is a field of a local event object handed to a callback,
// not state Reporter holds, so its try cannot leave Reporter stuck.
struct Event {
Phase stage;
};
void notify(const Event&);
class Reporter {
public:
void run() {
Event ev;
ev.stage = Phase::Starting;
notify(ev);
try {
mayThrow();
} catch (...) {
}
}
};
} // namespace fx