swactor/tools/actor-control-flow-lint/driver.rs

354 lines
11 KiB
Rust
Raw Normal View History

#![feature(rustc_private)]
extern crate rustc_driver;
extern crate rustc_hir;
extern crate rustc_interface;
extern crate rustc_middle;
extern crate rustc_span;
use std::env;
use std::process::ExitCode;
use rustc_driver::{Callbacks, Compilation};
use rustc_hir as hir;
use rustc_hir::def::Res;
use rustc_hir::intravisit::{self, Visitor};
use rustc_interface::interface::Compiler;
use rustc_middle::ty::{TyCtxt, TypeckResults};
use rustc_span::{Span, symbol::Symbol};
/// Crates whose purpose is to run the actor engine or turn external I/O,
/// process, telemetry, and provider API streams into actor observations.
/// Adding an entry changes the architecture.
const EXECUTION_OWNERS: &[&str] = &[
"dashboard",
"iroh-driver",
"swactor",
"swactor-engine",
"swactor-process",
"swactor-transport",
"swactor-vastai",
"telemetry",
];
/// This package owns only the compile-contract subprocess harness. It cannot be
/// used as a workspace dependency.
const TEST_SUPPORT_OWNERS: &[&str] = &["actor-control-flow-lint-tests"];
/// Policy-bearing crates that must never enter an execution owner's dependency
/// closure.
const DOMAIN_CONTROL_CRATES: &[&str] = &[
"myelin",
"provisioning",
"swactor-job-runner",
"xtask",
];
#[derive(Clone, Copy)]
struct Capability {
label: &'static str,
resolution: &'static str,
test_wait: bool,
paths: &'static [&'static str],
}
const MOVE_TO_OWNER: &str =
"move stream mechanics into an approved execution owner or move the decision into an actor";
const USE_ACTOR_TIMER: &str =
"schedule a typed actor message through the engine; the receiving actor owns the deadline decision";
/// Stable resolved item paths. These are deliberately compiler identities, not
/// spellings found in source, so re-exports, renamed imports, and local wrappers
/// cannot evade the boundary.
const CAPABILITIES: &[Capability] = &[
Capability {
label: "asynchronous task spawning",
resolution: MOVE_TO_OWNER,
test_wait: false,
paths: &[
"tokio::runtime::Handle::spawn",
"tokio::runtime::Runtime::spawn",
"tokio::spawn",
"tokio::task::spawn",
"tokio::task::spawn_local",
],
},
Capability {
label: "blocking task spawning",
resolution: MOVE_TO_OWNER,
test_wait: false,
paths: &[
"tokio::runtime::Handle::spawn_blocking",
"tokio::runtime::Runtime::spawn_blocking",
"tokio::task::spawn_blocking",
],
},
Capability {
label: "engine task scheduling",
resolution: MOVE_TO_OWNER,
test_wait: false,
paths: &["swactor_engine::EngineHandle::spawn"],
},
Capability {
label: "OS thread creation",
resolution: MOVE_TO_OWNER,
test_wait: false,
paths: &[
"std::thread::Builder::spawn",
"std::thread::Builder::spawn_unchecked",
"std::thread::spawn",
],
},
Capability {
label: "thread sleeping",
resolution: USE_ACTOR_TIMER,
test_wait: true,
paths: &[
"std::thread::park",
"std::thread::park_timeout",
"std::thread::sleep",
],
},
Capability {
label: "direct timer driving",
resolution: USE_ACTOR_TIMER,
test_wait: false,
paths: &[
"swactor_engine::EngineHandle::interval",
"swactor_engine::EngineHandle::timer",
"swactor_engine::EngineHandle::timeout",
"tokio::time::interval",
"tokio::time::interval_at",
"tokio::time::sleep",
"tokio::time::sleep_until",
"tokio::time::timeout",
"tokio::time::timeout_at",
],
},
Capability {
label: "runtime construction or driving",
resolution: "the actor engine owns runtime construction and progression",
test_wait: false,
paths: &[
"swactor::runtime::SingleThreadRuntime::tick",
"swactor::runtime::SingleThreadRuntime::try_tick",
"swactor::runtime::SingleThreadRuntime::has_work",
"tokio::runtime::Builder::new_current_thread",
"tokio::runtime::Builder::new_multi_thread",
"tokio::runtime::Handle::block_on",
"tokio::runtime::Runtime::block_on",
"tokio::runtime::Runtime::new",
],
},
Capability {
label: "blocking receive used as a controller",
resolution: "receive observations in an actor; tests may use a bounded observation wait",
test_wait: true,
paths: &[
"crossbeam_channel::channel::Receiver::recv",
"crossbeam_channel::channel::Receiver::recv_deadline",
"crossbeam_channel::channel::Receiver::recv_timeout",
"std::sync::mpsc::Receiver::recv",
"std::sync::mpsc::Receiver::recv_deadline",
"std::sync::mpsc::Receiver::recv_timeout",
"tokio::sync::mpsc::bounded::Receiver::blocking_recv",
"tokio::sync::oneshot::Receiver::blocking_recv",
],
},
Capability {
label: "process creation",
resolution: "send a command to the process I/O owner and return exit/output observations to an actor",
test_wait: false,
paths: &[
"std::process::Child::kill",
"std::process::Child::try_wait",
"std::process::Child::wait",
"std::process::Child::wait_with_output",
"std::process::Command::output",
"std::process::Command::spawn",
"std::process::Command::status",
"tokio::process::Child::kill",
"tokio::process::Child::start_kill",
"tokio::process::Child::try_wait",
"tokio::process::Child::wait",
"tokio::process::Child::wait_with_output",
"tokio::process::Command::output",
"tokio::process::Command::spawn",
"tokio::process::Command::status",
],
},
];
struct ActorControlFlowCallbacks {
package: String,
test_build: bool,
trace: bool,
}
impl Callbacks for ActorControlFlowCallbacks {
fn after_analysis<'tcx>(
&mut self,
_compiler: &Compiler,
tcx: TyCtxt<'tcx>,
) -> Compilation {
if TEST_SUPPORT_OWNERS.contains(&self.package.as_str()) {
return Compilation::Continue;
}
if EXECUTION_OWNERS.contains(&self.package.as_str()) {
check_owner_dependencies(tcx, &self.package);
return Compilation::Continue;
}
for owner in tcx.hir_body_owners() {
let typeck = tcx.typeck(owner);
let body = tcx.hir_body_owned_by(owner);
let mut visitor = CapabilityVisitor {
tcx,
typeck,
package: &self.package,
test_build: self.test_build,
trace: self.trace,
};
visitor.visit_body(body);
}
Compilation::Continue
}
}
fn check_owner_dependencies(tcx: TyCtxt<'_>, package: &str) {
for &crate_num in tcx.crates(()) {
let dependency_symbol = tcx.crate_name(crate_num);
let dependency = dependency_symbol.as_str();
if DOMAIN_CONTROL_CRATES.contains(&dependency) {
tcx.dcx().err(format!(
"actor control-flow policy: execution owner `{package}` depends on domain-control crate `{dependency}`; execution owners must remain below domain policy in the dependency graph"
));
}
}
}
struct CapabilityVisitor<'a, 'tcx> {
tcx: TyCtxt<'tcx>,
typeck: &'tcx TypeckResults<'tcx>,
package: &'a str,
test_build: bool,
trace: bool,
}
impl<'tcx> Visitor<'tcx> for CapabilityVisitor<'_, 'tcx> {
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) {
match &expr.kind {
hir::ExprKind::MethodCall(..) => {
if let Some(def_id) = self.typeck.type_dependent_def_id(expr.hir_id) {
self.check(def_id, expr.span);
}
}
hir::ExprKind::Path(qpath) => {
if let Res::Def(_, def_id) = self.typeck.qpath_res(qpath, expr.hir_id) {
self.check(def_id, expr.span);
}
}
_ => {}
}
intravisit::walk_expr(self, expr);
}
}
impl CapabilityVisitor<'_, '_> {
fn check(&self, def_id: rustc_hir::def_id::DefId, span: Span) {
let path = self.tcx.def_path_str(def_id);
let normalized_path = normalize_def_path(&path);
if self.trace && is_candidate_name(self.tcx.item_name(def_id)) {
eprintln!("actor-control-flow trace: {path}");
}
let Some(capability) = CAPABILITIES
.iter()
.find(|capability| capability.paths.contains(&normalized_path.as_str()))
else {
return;
};
if self.test_build && capability.test_wait {
return;
}
self.tcx.dcx().span_err(
span,
format!(
"actor control-flow violation: `{}` is forbidden in workspace crate `{}`; {}",
capability.label, self.package, capability.resolution
),
);
}
}
fn normalize_def_path(path: &str) -> String {
let mut normalized = String::with_capacity(path.len());
let mut cursor = 0;
while let Some(relative_start) = path[cursor..].find("::<") {
let start = cursor + relative_start;
normalized.push_str(&path[cursor..start]);
let generic_start = start + 3;
let mut depth = 1_usize;
let mut end = path.len();
for (offset, character) in path[generic_start..].char_indices() {
match character {
'<' => depth += 1,
'>' => {
depth -= 1;
if depth == 0 {
end = generic_start + offset + character.len_utf8();
break;
}
}
_ => {}
}
}
cursor = end;
}
normalized.push_str(&path[cursor..]);
normalized
}
fn is_candidate_name(name: Symbol) -> bool {
matches!(
name.as_str(),
"block_on"
| "blocking_recv"
| "has_work"
| "interval"
| "interval_at"
| "new_current_thread"
| "new_multi_thread"
| "recv"
| "recv_deadline"
| "recv_timeout"
| "sleep"
| "sleep_until"
| "spawn"
| "spawn_blocking"
| "spawn_local"
| "tick"
| "timeout"
| "timeout_at"
| "timer"
| "try_tick"
)
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let package = env::var("MYELIN_ACTOR_LINT_PACKAGE").unwrap_or_else(|_| "unknown".to_owned());
let test_build = env::var_os("MYELIN_ACTOR_LINT_TEST_BUILD").is_some();
let trace = env::var_os("MYELIN_ACTOR_LINT_TRACE").is_some();
let mut callbacks = ActorControlFlowCallbacks {
package,
test_build,
trace,
};
rustc_driver::catch_with_exit_code(|| rustc_driver::run_compiler(&args, &mut callbacks))
}