按表达式依赖裁剪策略前置声明
This commit is contained in:
@@ -811,7 +811,71 @@ impl<'a> SelectiveExpressionScope<'a> {
|
||||
struct ExpressionEvalPlan {
|
||||
identifiers: BTreeSet<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 {
|
||||
@@ -850,7 +914,7 @@ pub struct PlatformExprStrategy {
|
||||
cache_hits: RefCell<u64>,
|
||||
cache_misses: RefCell<u64>,
|
||||
expression_plan_cache: RefCell<HashMap<String, Arc<ExpressionEvalPlan>>>,
|
||||
prelude_runtime_template: Result<RuntimeExpressionTemplate, String>,
|
||||
prelude_dependency_plan: PreludeDependencyPlan,
|
||||
prelude_identifier_candidates: BTreeSet<String>,
|
||||
prelude_declared_identifiers: BTreeSet<String>,
|
||||
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("code_number", code_number_value);
|
||||
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 =
|
||||
Self::extract_identifier_candidates(&normalized_prelude);
|
||||
let prelude_declared_identifiers = Self::declared_prelude_identifiers(&config.prelude);
|
||||
@@ -1161,7 +1225,7 @@ impl PlatformExprStrategy {
|
||||
cache_hits: RefCell::new(0),
|
||||
cache_misses: RefCell::new(0),
|
||||
expression_plan_cache: RefCell::new(HashMap::new()),
|
||||
prelude_runtime_template,
|
||||
prelude_dependency_plan,
|
||||
prelude_identifier_candidates,
|
||||
prelude_declared_identifiers,
|
||||
stock_filter_quote_usage,
|
||||
@@ -4581,12 +4645,7 @@ impl PlatformExprStrategy {
|
||||
) -> Result<Dynamic, BacktestError> {
|
||||
let expression_plan = self.expression_eval_plan(expr);
|
||||
let normalized_identifiers = &expression_plan.identifiers;
|
||||
let empty_prelude_identifiers = BTreeSet::new();
|
||||
let prelude_identifiers = if expression_plan.requires_prelude {
|
||||
&self.prelude_identifier_candidates
|
||||
} else {
|
||||
&empty_prelude_identifiers
|
||||
};
|
||||
let prelude_identifiers = &expression_plan.prelude_identifiers;
|
||||
let include_day_factors = normalized_identifiers.contains("day_factors")
|
||||
|| normalized_identifiers.contains("day_factor")
|
||||
|| prelude_identifiers.contains("day_factors");
|
||||
@@ -4608,16 +4667,11 @@ impl PlatformExprStrategy {
|
||||
include_factors_map,
|
||||
include_process_event_counts,
|
||||
);
|
||||
let expanded_prelude = if expression_plan.requires_prelude {
|
||||
self.expand_runtime_helper_template(
|
||||
ctx,
|
||||
day,
|
||||
stock,
|
||||
&self.prelude_runtime_template,
|
||||
&mut scope,
|
||||
)?
|
||||
} else {
|
||||
String::new()
|
||||
let expanded_prelude = match expression_plan.prelude_runtime_template.as_ref() {
|
||||
Some(template) => {
|
||||
self.expand_runtime_helper_template(ctx, day, stock, template, &mut scope)?
|
||||
}
|
||||
None => String::new(),
|
||||
};
|
||||
let expanded_expr = self.expand_runtime_helper_template(
|
||||
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() {
|
||||
script_parts.push(expanded_prelude.as_str());
|
||||
}
|
||||
@@ -4669,12 +4727,18 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
let normalized = Self::normalize_expr(expr);
|
||||
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 {
|
||||
requires_prelude: identifiers
|
||||
.iter()
|
||||
.any(|identifier| self.prelude_declared_identifiers.contains(identifier)),
|
||||
identifiers,
|
||||
runtime_template: Self::compile_runtime_helper_template(&normalized),
|
||||
prelude_source,
|
||||
prelude_identifiers,
|
||||
prelude_runtime_template,
|
||||
});
|
||||
self.expression_plan_cache
|
||||
.borrow_mut()
|
||||
@@ -4722,6 +4786,156 @@ impl PlatformExprStrategy {
|
||||
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(¶meter);
|
||||
}
|
||||
Some(PreludeStatementPlan {
|
||||
source: trimmed.to_string(),
|
||||
declared_name,
|
||||
referenced_identifiers,
|
||||
})
|
||||
}
|
||||
|
||||
fn normalize_prelude_for_eval(prelude: &str) -> String {
|
||||
prelude
|
||||
.lines()
|
||||
@@ -31191,30 +31405,52 @@ fn passes_threshold(value) { value > stock_threshold }
|
||||
.to_string();
|
||||
let strategy = PlatformExprStrategy::new(cfg);
|
||||
|
||||
let stock_only = strategy.expression_eval_plan("close > 0 && !is_st");
|
||||
assert!(
|
||||
!strategy
|
||||
.expression_eval_plan("close > 0 && !is_st")
|
||||
.requires_prelude,
|
||||
stock_only.prelude_source.is_empty(),
|
||||
"stock-only expressions must not execute unrelated day-level prelude declarations"
|
||||
);
|
||||
let direct_variable = strategy.expression_eval_plan("market_cap > stock_threshold");
|
||||
assert!(
|
||||
strategy
|
||||
.expression_eval_plan("market_cap > stock_threshold")
|
||||
.requires_prelude,
|
||||
direct_variable
|
||||
.prelude_source
|
||||
.contains("let stock_threshold"),
|
||||
"direct prelude variable dependencies must retain the prelude"
|
||||
);
|
||||
assert!(
|
||||
strategy
|
||||
.expression_eval_plan("market_cap > constant_threshold")
|
||||
.requires_prelude,
|
||||
!direct_variable
|
||||
.prelude_source
|
||||
.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"
|
||||
);
|
||||
assert!(
|
||||
strategy
|
||||
.expression_eval_plan("passes_threshold(market_cap)")
|
||||
.requires_prelude,
|
||||
!direct_constant
|
||||
.prelude_source
|
||||
.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"
|
||||
);
|
||||
assert!(
|
||||
function_dependency
|
||||
.prelude_source
|
||||
.contains("let stock_threshold")
|
||||
);
|
||||
assert!(
|
||||
!function_dependency
|
||||
.prelude_source
|
||||
.contains("unrelated_market_signal")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user