Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repository = "https://github.com/wrightkit/wright"

[workspace.dependencies]
# Canonical Workshop core; SemVer-compatible requirement on the crates.io release.
workshop-rs = "1.0.0"
workshop-rs = "1.1.0"
libc = "0.2"
serde = "1"
serde_json = "1"
Expand Down
63 changes: 33 additions & 30 deletions crates/wright-analyzer/src/canonical/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use workshop_rs::source::Span;
use workshop_rs::{Action, Event, ModifyOp, Program, Rule, Value};

use super::cfg::{is_wait, matching_end};
use super::symbols::{ActionId, RuleId, ValueId, source_occurrence, value_identity_map};
use super::symbols::{ActionId, RuleId, ValueId, value_identity_map};
use super::traversal::{visit_action_roots, visit_value_tree};
use crate::analysis::{Boundedness, EvidenceClass, Severity};
use crate::registry::LintConfig;
Expand Down Expand Up @@ -45,24 +45,28 @@ pub fn analyze(program: &Program, config: &LintConfig) -> Vec<Finding> {
}
if config.is_enabled("expensive-loop-check") {
for (offset, body_action) in body.iter().enumerate() {
let body_action_id = start + offset;
let mut expensive = Vec::new();
visit_action_roots(body_action, &mut |_, value| {
collect_expensive_values(value, &mut expensive)
visit_action_roots(body_action, &mut |argument, value| {
for (path, value) in collect_expensive_values(value) {
expensive.push((argument, path, value));
}
});
for value in expensive {
let Value::Call { name, .. } = value else {
unreachable!("only expensive calls are collected")
};
for (argument, path, value) in expensive {
debug_assert!(
matches!(value, Value::Call { .. }),
"only expensive calls are collected"
);
findings.push(Finding {
code: "expensive-loop-check".into(),
severity: Severity::Info,
message: "geometry predicate evaluated inside a loop body may be expensive per iteration"
.into(),
span: source_occurrence(
program,
program.action_span(rule_id, start + offset),
name,
false,
span: program.action_argument_value_span(
rule_id,
body_action_id,
argument,
&path,
),
rule: rule_id,
action: Some(action_id),
Expand Down Expand Up @@ -202,9 +206,8 @@ fn ongoing_condition_findings(
let condition_count = active_conditions.len();
let mut findings = Vec::new();
for (index, &(source_index, condition)) in active_conditions.iter().enumerate() {
let mut expensive = Vec::new();
collect_expensive_values(&condition.value, &mut expensive);
for value in expensive {
let expensive = collect_expensive_values(&condition.value);
for (path, value) in expensive {
let preceding = index;
let later = condition_count - index - 1;
let evaluation = match preceding {
Expand All @@ -220,19 +223,15 @@ fn ongoing_condition_findings(
if later == 1 { "" } else { "s" }
)
};
let name = match value {
Value::Call { name, .. } => name,
_ => unreachable!("only expensive call values are collected"),
};
let span = program.condition_span(rule_id, source_index);
let span = program.condition_value_span(rule_id, source_index, &path);
findings.push(Finding {
code: "ongoing-condition-hot-path".into(),
severity: Severity::Info,
message: format!(
"geometry predicate in an ongoing-rule condition {} of {condition_count} {evaluation}{later_gates}; its cost is heuristic, not measured runtime load",
index + 1,
),
span: source_occurrence(program, span, name, false),
span,
rule: rule_id,
action: None,
value: value_ids.get(&(value as *const Value as usize)).copied(),
Expand All @@ -244,15 +243,17 @@ fn ongoing_condition_findings(
findings
}

fn collect_expensive_values<'a>(value: &'a Value, out: &mut Vec<&'a Value>) {
visit_value_tree(value, None, &mut |value, _| {
fn collect_expensive_values(value: &Value) -> Vec<(Vec<usize>, &Value)> {
let mut out = Vec::new();
visit_value_tree(value, None, &mut |value, _, path| {
if let Value::Call { name, .. } = value {
if ["distance", "raycast", "isInLoS"].contains(&name.as_str()) {
out.push(value);
out.push((path.to_vec(), value));
}
}
0
});
out
}

fn duplicate_condition_findings(
Expand Down Expand Up @@ -309,7 +310,7 @@ fn repeated_value_findings(
collect_value_tree(
condition,
None,
program.action_argument_span(rule_id, loop_action, 0),
&mut |path| program.action_argument_value_span(rule_id, loop_action, 0, path),
&mut values,
&mut parents,
&mut spans,
Expand All @@ -332,7 +333,9 @@ fn repeated_value_findings(
collect_value_tree(
value,
None,
program.action_argument_span(rule_id, action_id, argument),
&mut |path| {
program.action_argument_value_span(rule_id, action_id, argument, path)
},
&mut values,
&mut parents,
&mut spans,
Expand Down Expand Up @@ -369,16 +372,16 @@ fn repeated_value_findings(
fn collect_value_tree<'a>(
value: &'a Value,
parent: Option<usize>,
span: Option<Span>,
span_at: &mut impl FnMut(&[usize]) -> Option<Span>,
values: &mut Vec<&'a Value>,
parents: &mut Vec<Option<usize>>,
spans: &mut Vec<Option<Span>>,
) {
visit_value_tree(value, parent, &mut |value, parent| {
visit_value_tree(value, parent, &mut |value, parent, path| {
let index = values.len();
values.push(value);
parents.push(parent);
spans.push(span);
spans.push(span_at(path));
index
});
}
Expand Down Expand Up @@ -432,7 +435,7 @@ fn duplicated_value_families(values: &[&Value], parents: &[Option<usize>]) -> Ve

fn value_call_count(value: &Value) -> usize {
let mut count = 0;
visit_value_tree(value, None, &mut |value, _| {
visit_value_tree(value, None, &mut |value, _, _| {
count += usize::from(matches!(value, Value::Call { .. }));
0
});
Expand Down
Loading
Loading