按表达式依赖裁剪策略前置声明
This commit is contained in:
@@ -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(¶meter);
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
|||||||
Reference in New Issue
Block a user