From a79077af1748d50ab8d716c57ede1a7a742b7007 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 24 Aug 2026 10:05:46 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=E8=A1=A8=E8=BE=BE=E5=BC=8F=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E8=A3=81=E5=89=AA=E7=AD=96=E7=95=A5=E5=89=8D=E7=BD=AE?= =?UTF-8?q?=E5=A3=B0=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fidc-core/src/platform_expr_strategy.rs | 308 ++++++++++++++++-- 1 file changed, 272 insertions(+), 36 deletions(-) diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index ebe9119..77344de 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -811,7 +811,71 @@ impl<'a> SelectiveExpressionScope<'a> { struct ExpressionEvalPlan { identifiers: BTreeSet, runtime_template: Result, - requires_prelude: bool, + prelude_source: String, + prelude_identifiers: BTreeSet, + prelude_runtime_template: Option>, +} + +struct PreludeDependencyPlan { + full_source: String, + statements: Vec, + declarative: bool, +} + +struct PreludeStatementPlan { + source: String, + declared_name: String, + referenced_identifiers: BTreeSet, +} + +impl PreludeDependencyPlan { + fn source_for_expression(&self, expression_identifiers: &BTreeSet) -> 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::>(); + 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::>() + .join("\n") + } } struct RuntimeExpressionTemplate { @@ -850,7 +914,7 @@ pub struct PlatformExprStrategy { cache_hits: RefCell, cache_misses: RefCell, expression_plan_cache: RefCell>>, - prelude_runtime_template: Result, + prelude_dependency_plan: PreludeDependencyPlan, prelude_identifier_candidates: BTreeSet, prelude_declared_identifiers: BTreeSet, 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 { 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::>() + .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 { + 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 { + 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::(); + 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::(); + 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::>(); + 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]