按表达式依赖裁剪策略前置声明

This commit is contained in:
boris
2026-08-24 10:05:46 +08:00
parent 61a4172bd4
commit a79077af17
+272 -36
View File
@@ -811,7 +811,71 @@ impl<'a> SelectiveExpressionScope<'a> {
struct ExpressionEvalPlan { struct ExpressionEvalPlan {
identifiers: BTreeSet<String>, identifiers: BTreeSet<String>,
runtime_template: Result<RuntimeExpressionTemplate, String>, runtime_template: Result<RuntimeExpressionTemplate, String>,
requires_prelude: bool, prelude_source: String,
prelude_identifiers: BTreeSet<String>,
prelude_runtime_template: Option<Result<RuntimeExpressionTemplate, String>>,
}
struct PreludeDependencyPlan {
full_source: String,
statements: Vec<PreludeStatementPlan>,
declarative: bool,
}
struct PreludeStatementPlan {
source: String,
declared_name: String,
referenced_identifiers: BTreeSet<String>,
}
impl PreludeDependencyPlan {
fn source_for_expression(&self, expression_identifiers: &BTreeSet<String>) -> String {
if self.full_source.trim().is_empty() {
return String::new();
}
let declared_names = self
.statements
.iter()
.map(|statement| statement.declared_name.as_str())
.collect::<BTreeSet<_>>();
if !expression_identifiers
.iter()
.any(|identifier| declared_names.contains(identifier.as_str()))
{
return if self.declarative {
String::new()
} else {
self.full_source.clone()
};
}
if !self.declarative {
return self.full_source.clone();
}
let mut required = expression_identifiers.clone();
let mut included = vec![false; self.statements.len()];
loop {
let mut changed = false;
for (index, statement) in self.statements.iter().enumerate() {
if included[index] || !required.contains(&statement.declared_name) {
continue;
}
included[index] = true;
changed = true;
required.extend(statement.referenced_identifiers.iter().cloned());
}
if !changed {
break;
}
}
self.statements
.iter()
.zip(included)
.filter_map(|(statement, included)| included.then_some(statement.source.as_str()))
.collect::<Vec<_>>()
.join("\n")
}
} }
struct RuntimeExpressionTemplate { struct RuntimeExpressionTemplate {
@@ -850,7 +914,7 @@ pub struct PlatformExprStrategy {
cache_hits: RefCell<u64>, cache_hits: RefCell<u64>,
cache_misses: RefCell<u64>, cache_misses: RefCell<u64>,
expression_plan_cache: RefCell<HashMap<String, Arc<ExpressionEvalPlan>>>, expression_plan_cache: RefCell<HashMap<String, Arc<ExpressionEvalPlan>>>,
prelude_runtime_template: Result<RuntimeExpressionTemplate, String>, prelude_dependency_plan: PreludeDependencyPlan,
prelude_identifier_candidates: BTreeSet<String>, prelude_identifier_candidates: BTreeSet<String>,
prelude_declared_identifiers: BTreeSet<String>, prelude_declared_identifiers: BTreeSet<String>,
stock_filter_quote_usage: StockFilterQuoteUsage, stock_filter_quote_usage: StockFilterQuoteUsage,
@@ -1121,7 +1185,7 @@ impl PlatformExprStrategy {
engine.register_fn("strlen", |value: &str| value.chars().count() as i64); engine.register_fn("strlen", |value: &str| value.chars().count() as i64);
engine.register_fn("code_number", code_number_value); engine.register_fn("code_number", code_number_value);
let normalized_prelude = Self::normalize_prelude_for_eval(&config.prelude); let normalized_prelude = Self::normalize_prelude_for_eval(&config.prelude);
let prelude_runtime_template = Self::compile_runtime_helper_template(&normalized_prelude); let prelude_dependency_plan = Self::compile_prelude_dependency_plan(&normalized_prelude);
let prelude_identifier_candidates = let prelude_identifier_candidates =
Self::extract_identifier_candidates(&normalized_prelude); Self::extract_identifier_candidates(&normalized_prelude);
let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude); let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude);
@@ -1161,7 +1225,7 @@ impl PlatformExprStrategy {
cache_hits: RefCell::new(0), cache_hits: RefCell::new(0),
cache_misses: RefCell::new(0), cache_misses: RefCell::new(0),
expression_plan_cache: RefCell::new(HashMap::new()), expression_plan_cache: RefCell::new(HashMap::new()),
prelude_runtime_template, prelude_dependency_plan,
prelude_identifier_candidates, prelude_identifier_candidates,
prelude_declared_identifiers, prelude_declared_identifiers,
stock_filter_quote_usage, stock_filter_quote_usage,
@@ -4581,12 +4645,7 @@ impl PlatformExprStrategy {
) -> Result<Dynamic, BacktestError> { ) -> Result<Dynamic, BacktestError> {
let expression_plan = self.expression_eval_plan(expr); let expression_plan = self.expression_eval_plan(expr);
let normalized_identifiers = &expression_plan.identifiers; let normalized_identifiers = &expression_plan.identifiers;
let empty_prelude_identifiers = BTreeSet::new(); let prelude_identifiers = &expression_plan.prelude_identifiers;
let prelude_identifiers = if expression_plan.requires_prelude {
&self.prelude_identifier_candidates
} else {
&empty_prelude_identifiers
};
let include_day_factors = normalized_identifiers.contains("day_factors") let include_day_factors = normalized_identifiers.contains("day_factors")
|| normalized_identifiers.contains("day_factor") || normalized_identifiers.contains("day_factor")
|| prelude_identifiers.contains("day_factors"); || prelude_identifiers.contains("day_factors");
@@ -4608,16 +4667,11 @@ impl PlatformExprStrategy {
include_factors_map, include_factors_map,
include_process_event_counts, include_process_event_counts,
); );
let expanded_prelude = if expression_plan.requires_prelude { let expanded_prelude = match expression_plan.prelude_runtime_template.as_ref() {
self.expand_runtime_helper_template( Some(template) => {
ctx, self.expand_runtime_helper_template(ctx, day, stock, template, &mut scope)?
day, }
stock, None => String::new(),
&self.prelude_runtime_template,
&mut scope,
)?
} else {
String::new()
}; };
let expanded_expr = self.expand_runtime_helper_template( let expanded_expr = self.expand_runtime_helper_template(
ctx, ctx,
@@ -4654,7 +4708,11 @@ impl PlatformExprStrategy {
} }
} }
} }
let mut script_parts = Vec::with_capacity(2); let mut script_parts = Vec::with_capacity(if expression_plan.prelude_source.is_empty() {
1
} else {
2
});
if !expanded_prelude.trim().is_empty() { if !expanded_prelude.trim().is_empty() {
script_parts.push(expanded_prelude.as_str()); script_parts.push(expanded_prelude.as_str());
} }
@@ -4669,12 +4727,18 @@ impl PlatformExprStrategy {
} }
let normalized = Self::normalize_expr(expr); let normalized = Self::normalize_expr(expr);
let identifiers = Self::extract_identifier_candidates(&normalized); let identifiers = Self::extract_identifier_candidates(&normalized);
let prelude_source = self
.prelude_dependency_plan
.source_for_expression(&identifiers);
let prelude_identifiers = Self::extract_identifier_candidates(&prelude_source);
let prelude_runtime_template = (!prelude_source.trim().is_empty())
.then(|| Self::compile_runtime_helper_template(&prelude_source));
let plan = Arc::new(ExpressionEvalPlan { let plan = Arc::new(ExpressionEvalPlan {
requires_prelude: identifiers
.iter()
.any(|identifier| self.prelude_declared_identifiers.contains(identifier)),
identifiers, identifiers,
runtime_template: Self::compile_runtime_helper_template(&normalized), runtime_template: Self::compile_runtime_helper_template(&normalized),
prelude_source,
prelude_identifiers,
prelude_runtime_template,
}); });
self.expression_plan_cache self.expression_plan_cache
.borrow_mut() .borrow_mut()
@@ -4722,6 +4786,156 @@ impl PlatformExprStrategy {
output output
} }
fn compile_prelude_dependency_plan(normalized_prelude: &str) -> PreludeDependencyPlan {
let full_source = normalized_prelude
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
if full_source.trim().is_empty() {
return PreludeDependencyPlan {
full_source,
statements: Vec::new(),
declarative: true,
};
}
let raw_statements = Self::split_prelude_statements(&full_source);
let mut statements = Vec::with_capacity(raw_statements.len());
let mut declarative = !raw_statements.is_empty();
for source in raw_statements {
let Some(statement) = Self::parse_prelude_statement(&source) else {
declarative = false;
continue;
};
statements.push(statement);
}
PreludeDependencyPlan {
full_source,
statements,
declarative,
}
}
fn split_prelude_statements(prelude: &str) -> Vec<String> {
let mut statements = Vec::new();
let mut start = 0usize;
let mut brace_depth = 0i32;
let mut paren_depth = 0i32;
let mut bracket_depth = 0i32;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut escaped = false;
let mut in_line_comment = false;
let mut chars = prelude.char_indices().peekable();
while let Some((idx, ch)) = chars.next() {
if in_line_comment {
if ch == '\n' {
in_line_comment = false;
}
continue;
}
if escaped {
escaped = false;
continue;
}
if ch == '\\' && (in_single_quote || in_double_quote) {
escaped = true;
continue;
}
if ch == '\'' && !in_double_quote {
in_single_quote = !in_single_quote;
continue;
}
if ch == '"' && !in_single_quote {
in_double_quote = !in_double_quote;
continue;
}
if in_single_quote || in_double_quote {
continue;
}
if ch == '/' && chars.peek().is_some_and(|(_, next)| *next == '/') {
in_line_comment = true;
continue;
}
match ch {
'{' => brace_depth += 1,
'}' => brace_depth -= 1,
'(' => paren_depth += 1,
')' => paren_depth -= 1,
'[' => bracket_depth += 1,
']' => bracket_depth -= 1,
_ => {}
}
let at_top_level = brace_depth == 0 && paren_depth == 0 && bracket_depth == 0;
let statement_end = if ch == ';' && at_top_level {
true
} else if ch == '}' && at_top_level {
prelude[start..=idx].trim_start().starts_with("fn ")
} else {
false
};
if statement_end {
let source = prelude[start..idx + ch.len_utf8()].trim();
if !source.is_empty() {
statements.push(source.to_string());
}
start = idx + ch.len_utf8();
}
}
let tail = prelude[start..].trim();
if !tail.is_empty() {
statements.push(tail.to_string());
}
statements
}
fn parse_prelude_statement(source: &str) -> Option<PreludeStatementPlan> {
let trimmed = source.trim();
let (declared_name, parameters) = if let Some(body) = trimmed
.strip_prefix("let ")
.or_else(|| trimmed.strip_prefix("const "))
{
let name = body
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.collect::<String>();
if name.is_empty() || !body[name.len()..].contains('=') {
return None;
}
(name, BTreeSet::new())
} else if let Some(body) = trimmed.strip_prefix("fn ") {
let name = body
.chars()
.take_while(|ch| ch.is_ascii_alphanumeric() || *ch == '_')
.collect::<String>();
let open = body.find('(')?;
let close = body[open + 1..].find(')')? + open + 1;
let parameters = body[open + 1..close]
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.collect::<BTreeSet<_>>();
if name.is_empty() {
return None;
}
(name, parameters)
} else {
return None;
};
let mut referenced_identifiers = Self::extract_identifier_candidates(trimmed);
referenced_identifiers.remove(&declared_name);
for parameter in parameters {
referenced_identifiers.remove(&parameter);
}
Some(PreludeStatementPlan {
source: trimmed.to_string(),
declared_name,
referenced_identifiers,
})
}
fn normalize_prelude_for_eval(prelude: &str) -> String { fn normalize_prelude_for_eval(prelude: &str) -> String {
prelude prelude
.lines() .lines()
@@ -31191,30 +31405,52 @@ fn passes_threshold(value) { value > stock_threshold }
.to_string(); .to_string();
let strategy = PlatformExprStrategy::new(cfg); let strategy = PlatformExprStrategy::new(cfg);
let stock_only = strategy.expression_eval_plan("close > 0 && !is_st");
assert!( assert!(
!strategy stock_only.prelude_source.is_empty(),
.expression_eval_plan("close > 0 && !is_st")
.requires_prelude,
"stock-only expressions must not execute unrelated day-level prelude declarations" "stock-only expressions must not execute unrelated day-level prelude declarations"
); );
let direct_variable = strategy.expression_eval_plan("market_cap > stock_threshold");
assert!( assert!(
strategy direct_variable
.expression_eval_plan("market_cap > stock_threshold") .prelude_source
.requires_prelude, .contains("let stock_threshold"),
"direct prelude variable dependencies must retain the prelude" "direct prelude variable dependencies must retain the prelude"
); );
assert!( assert!(
strategy !direct_variable
.expression_eval_plan("market_cap > constant_threshold") .prelude_source
.requires_prelude, .contains("unrelated_market_signal")
);
let direct_constant = strategy.expression_eval_plan("market_cap > constant_threshold");
assert!(
direct_constant
.prelude_source
.contains("const constant_threshold"),
"const prelude dependencies must retain the prelude" "const prelude dependencies must retain the prelude"
); );
assert!( assert!(
strategy !direct_constant
.expression_eval_plan("passes_threshold(market_cap)") .prelude_source
.requires_prelude, .contains("unrelated_market_signal")
);
let function_dependency = strategy.expression_eval_plan("passes_threshold(market_cap)");
assert!(
function_dependency
.prelude_source
.contains("fn passes_threshold"),
"user-defined prelude function dependencies must retain the prelude" "user-defined prelude function dependencies must retain the prelude"
); );
assert!(
function_dependency
.prelude_source
.contains("let stock_threshold")
);
assert!(
!function_dependency
.prelude_source
.contains("unrelated_market_signal")
);
} }
#[test] #[test]