use std::collections::BTreeSet;
use super::bytecode::{Chunk, Const, Op};
include!(concat!(env!("OUT_DIR"), "/bridge_tables.rs"));
#[derive(Clone, Copy, PartialEq)]
pub enum Engine {
Fast,
Parallel,
Both,
}
pub struct BridgeTable {
pub engine: Engine,
pub recv: &'static str,
pub names: &'static [&'static str],
}
pub struct Finding {
pub method: String,
pub recv: Option<&'static str>,
pub func: String,
}
impl Finding {
pub fn message(&self) -> String {
match self.recv {
Some(recv) => format!(
"`{}` on {} is not implemented by the interpreter, in `{}`",
self.method, recv, self.func
),
None => format!(
"`{}` is not implemented by the interpreter, in `{}`",
self.method, self.func
),
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Ty {
Str,
Int,
Float,
Bool,
Char,
Vec,
Map,
Json,
Unknown,
}
impl Ty {
fn name(self) -> Option<&'static str> {
match self {
Ty::Str => Some("Str"),
Ty::Vec => Some("Vec"),
Ty::Map => Some("Map"),
Ty::Json => Some("Value"),
Ty::Int | Ty::Float | Ty::Bool | Ty::Char | Ty::Unknown => None,
}
}
fn from_annotation(name: &str) -> Ty {
match name {
"Value" => Ty::Json,
"String" | "str" => Ty::Str,
"Vec" | "VecDeque" => Ty::Vec,
"HashMap" | "BTreeMap" | "IndexMap" => Ty::Map,
_ => Ty::Unknown,
}
}
}
fn json_shapes(engine: Engine) -> &'static [&'static str] {
match engine {
Engine::Parallel => &["Map", "Vec", "Str", "Enum"],
_ => &["Map", "Vec", "Str", "Option"],
}
}
fn applies(table: &BridgeTable, engine: Engine) -> bool {
table.engine == engine || table.engine == Engine::Both
}
fn any_name(engine: Engine, method: &str) -> bool {
(engine == Engine::Fast && BUILTIN_IDS.contains(&method))
|| VM_BUILTINS.contains(&method)
|| BRIDGE_TABLES
.iter()
.any(|t| applies(t, engine) && t.names.contains(&method))
}
const VM_BUILTINS: &[&str] = &["clone_from", "push", "push_str", "parse"];
fn on_recv(engine: Engine, recv: &str, method: &str) -> bool {
if VM_BUILTINS.contains(&method) {
return true;
}
let mut saw_table = false;
for table in BRIDGE_TABLES.iter().filter(|t| applies(t, engine)) {
if table.recv == recv {
saw_table = true;
if table.names.contains(&method) {
return true;
}
}
if table.recv == "*" && table.names.contains(&method) {
return true;
}
}
if !saw_table {
return any_name(engine, method);
}
engine == Engine::Fast && BUILTIN_IDS.contains(&method)
}
const UNIVERSAL: &[&str] = &["clone", "to_string"];
#[derive(Clone, Copy, PartialEq)]
pub enum Avail {
Both,
FastOnly,
ParallelOnly,
}
pub fn surface() -> Vec<(&'static str, &'static str, Avail)> {
let mut merged: std::collections::BTreeMap<(&str, &str), (bool, bool)> =
std::collections::BTreeMap::new();
for table in BRIDGE_TABLES {
for name in table.names {
if name.contains(' ') || name.contains('`') || name.len() <= 1 {
continue;
}
let entry = merged.entry((table.recv, name)).or_insert((false, false));
if applies(table, Engine::Fast) {
entry.0 = true;
}
if applies(table, Engine::Parallel) {
entry.1 = true;
}
}
}
for name in BUILTIN_IDS {
if name.len() > 1 {
merged.insert(("builtin", name), (true, true));
}
}
merged
.into_iter()
.map(|((recv, name), (fast, parallel))| {
let avail = match (fast, parallel) {
(true, true) => Avail::Both,
(true, false) => Avail::FastOnly,
_ => Avail::ParallelOnly,
};
(recv, name, avail)
})
.collect()
}
fn walk(chunk: &Chunk, engine: Engine, user: &BTreeSet<String>, out: &mut Vec<Finding>) {
for (index, op) in chunk.code.iter().enumerate() {
if let Op::Method { recv, name, .. } = op {
let method = &chunk.names[*name as usize].text;
if UNIVERSAL.contains(&method.as_str()) || user.contains(method) {
continue;
}
let ty = infer(chunk, index, *recv);
let known = match ty {
Ty::Json => json_shapes(engine)
.iter()
.all(|shape| on_recv(engine, shape, method)),
_ => match ty.name() {
Some(recv_name) => on_recv(engine, recv_name, method),
None => any_name(engine, method),
},
};
if !known {
out.push(Finding {
method: method.clone(),
recv: ty.name(),
func: chunk.name.clone(),
});
}
}
}
for child in &chunk.children {
walk(child, engine, user, out);
}
}
fn infer(chunk: &Chunk, before: usize, reg: u16) -> Ty {
for op in chunk.code[..before].iter().rev() {
match op {
Op::LoadConst { dst, k } if *dst == reg => {
return match chunk.consts[*k as usize] {
Const::Str(_) => Ty::Str,
Const::Char(_) => Ty::Char,
Const::Float(_) | Const::F32(_) => Ty::Float,
Const::Bytes(_) => Ty::Vec,
};
}
Op::LoadInt { dst, .. } if *dst == reg => return Ty::Int,
Op::LoadBool { dst, .. } if *dst == reg => return Ty::Bool,
Op::MakeVec { dst, .. } if *dst == reg => return Ty::Vec,
Op::Fmt { dst, .. } if *dst == reg => return Ty::Str,
_ => {
if writes(op) == Some(reg) {
return Ty::Unknown;
}
}
}
}
match chunk.param_types.get(reg as usize) {
Some(Some(name)) => Ty::from_annotation(name),
_ => Ty::Unknown,
}
}
fn writes(op: &Op) -> Option<u16> {
match op {
Op::Move { dst, .. }
| Op::Bin { dst, .. }
| Op::Un { dst, .. }
| Op::Method { dst, .. }
| Op::CallFn { dst, .. }
| Op::CallPath { dst, .. }
| Op::CallValue { dst, .. }
| Op::MakeStruct { dst, .. }
| Op::MakeEnum { dst, .. }
| Op::LoadGlobal { dst, .. }
| Op::LoadUpvalue { dst, .. }
| Op::LoadCell { dst, .. }
| Op::Index { dst, .. }
| Op::Deref { dst, .. }
| Op::GetField { dst, .. } => Some(*dst),
_ => None,
}
}
pub fn report(
functions: &[std::rc::Rc<Chunk>],
methods: impl Iterator<Item = String>,
engine: Engine,
) -> Vec<Finding> {
let user: BTreeSet<String> = methods.collect();
let mut out = Vec::new();
for chunk in functions {
walk(chunk, engine, &user, &mut out);
}
let mut seen = BTreeSet::new();
out.retain(|f| seen.insert((f.method.clone(), f.recv)));
out
}
#[cfg(test)]
mod tests {
use super::*;
fn engine_names(engine: Engine) -> BTreeSet<&'static str> {
BRIDGE_TABLES
.iter()
.filter(|t| applies(t, engine))
.flat_map(|t| t.names.iter().copied())
.filter(|n| !n.contains(' ') && !n.contains('`') && n.len() > 1)
.collect()
}
#[test]
fn a_fast_only_builtin_is_reported_for_the_parallel_engine() {
for method in ["sort_by_key", "retain", "fold", "map_err", "reduce"] {
assert!(
any_name(Engine::Fast, method),
"`{method}` must stay available on the fast engine"
);
assert!(
!any_name(Engine::Parallel, method),
"`{method}` is not implemented in tokio mode and must be reported"
);
}
assert!(on_recv(Engine::Fast, "Vec", "sort_by_key"));
assert!(!on_recv(Engine::Parallel, "Vec", "sort_by_key"));
for method in VM_BUILTINS {
assert!(on_recv(Engine::Parallel, "Str", method));
assert!(any_name(Engine::Parallel, method));
}
}
#[test]
fn a_json_value_is_checked_against_every_shape() {
assert!(on_recv(Engine::Parallel, "Map", "keys"));
assert!(
!json_shapes(Engine::Parallel).iter().all(|shape| on_recv(
Engine::Parallel,
shape,
"keys"
)),
"a map-only method must not pass for a json Value"
);
for engine in [Engine::Fast, Engine::Parallel] {
assert!(
json_shapes(engine)
.iter()
.all(|shape| on_recv(engine, shape, "get")),
"`get` has to work on every json shape"
);
}
}
#[test]
fn parallel_methods_outside_the_id_table_are_not_reported() {
for method in ["and_then", "is_some_and", "elapsed", "map_or", "or_else"] {
assert!(
any_name(Engine::Parallel, method),
"`{method}` runs in tokio mode and must not be reported"
);
}
}
#[test]
fn parallel_engine_gap_is_deliberate() {
let fast = engine_names(Engine::Fast);
let parallel = engine_names(Engine::Parallel);
let gap: BTreeSet<&str> = fast.difference(¶llel).copied().collect();
let known: BTreeSet<&str> = KNOWN_GAP.iter().copied().collect();
let new: Vec<&&str> = gap.difference(&known).collect();
let closed: Vec<&&str> = known.difference(&gap).collect();
assert!(
new.is_empty(),
"new fast-only methods. Port them to the parallel engine, or add \
them to KNOWN_GAP as a deliberate exclusion: {new:?}"
);
assert!(
closed.is_empty(),
"methods no longer fast-only, remove them from KNOWN_GAP: {closed:?}"
);
}
const KNOWN_GAP: &[&str] = &[
"accept",
"access",
"accessed",
"account_name",
"ancestors",
"and_modify",
"append",
"as_deref_mut",
"as_os_str",
"as_path",
"chain_update",
"change_config",
"change_page_content",
"close",
"connect",
"create_subkey",
"create",
"create_new",
"created",
"current_state",
"cwd",
"decode",
"dedup",
"delete_subkey",
"delete_subkey_all",
"delete_value",
"dependencies",
"dev",
"display",
"display_name",
"drain",
"duration_since",
"encode",
"enum_keys",
"enum_values",
"err",
"error_control",
"executable_path",
"exists",
"extension",
"file_name",
"file_stem",
"file_type",
"fill",
"fill_bytes",
"finalize",
"flags",
"flatten",
"fold",
"gen",
"gen_bool",
"gen_range",
"get_all",
"get_page_content",
"get_pages",
"get_raw_value",
"get_text",
"get_value",
"gid",
"incoming",
"inner",
"ino",
"into_os_string",
"is_absolute",
"is_dir",
"is_err_and",
"is_file",
"is_ok_and",
"is_symlink",
"key",
"local_addr",
"manager_access",
"map_err",
"max_by_key",
"metadata",
"min_by_key",
"mode",
"modified",
"mtime",
"namespace",
"ok_or",
"open",
"open_service",
"open_subkey",
"open_subkey_with_flags",
"or",
"or_default",
"or_insert",
"or_insert_with",
"or_insert_with_key",
"parent",
"partition",
"path",
"peek",
"peekable",
"peer_addr",
"permissions",
"query_config",
"query_status",
"random",
"random_bool",
"random_range",
"raw_query",
"read",
"read_to_end",
"readonly",
"redirect",
"reduce",
"retain",
"reverse",
"root",
"save",
"seek",
"send_to",
"service_type",
"set_broadcast",
"set_len",
"set_modified",
"set_raw_value",
"set_readonly",
"set_value",
"shutdown",
"skip_while",
"sort_by",
"sort_by_cached_key",
"sort_by_key",
"standard_no_pad",
"start_type",
"stop",
"sync_all",
"sync_data",
"take_while",
"then_some",
"to_path_buf",
"to_string_lossy",
"truncate",
"try_clone",
"try_wait",
"uid",
"unwrap_err",
"update",
"url_safe",
"url_safe_no_pad",
"values_mut",
"with_extension",
];
}