diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs index 37a60d86f..c29cd298f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs @@ -1412,12 +1412,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { &self, expression: &JavascriptExpression<'_>, ) -> Option { - let expression = javascript_unwrap_parenthesized_expression(expression); - let identifier = expression.get_identifier_reference()?; - identifier - .reference_id - .get() - .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) + javascript_referenced_value_symbol(expression, self.scoping) } fn record_binding_initializer( @@ -1996,6 +1991,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { struct JavascriptFunctionInvocationCollector<'a, 'b> { scoping: &'b JavascriptScoping, + known_promise_symbols: &'b BTreeSet, content: &'b str, context_ranges: &'b [NamedJavascriptFunctionRange], literal_false_ranges: &'b JavascriptLiteralFalseRangeIndex, @@ -2023,6 +2019,24 @@ fn javascript_unwrap_parenthesized_expression<'a, 'b>( expression } +fn javascript_referenced_value_symbol( + expression: &JavascriptExpression<'_>, + scoping: &JavascriptScoping, +) -> Option { + let expression = javascript_unwrap_parenthesized_expression(expression); + if let JavascriptExpression::SequenceExpression(sequence) = expression { + return sequence + .expressions + .last() + .and_then(|expression| javascript_referenced_value_symbol(expression, scoping)); + } + let identifier = expression.get_identifier_reference()?; + identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()) +} + fn javascript_identifier_is_unresolved_known_global( identifier: &oxc_ast::ast::IdentifierReference<'_>, scoping: &JavascriptScoping, @@ -2035,6 +2049,222 @@ fn javascript_identifier_is_unresolved_known_global( .is_some_and(|reference_id| scoping.get_reference(reference_id).symbol_id().is_none()) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum JavascriptLiteralValueKind { + Truthy, + Falsy, + Nullish, +} + +fn javascript_literal_value_kind( + expression: &JavascriptExpression<'_>, +) -> Option { + match javascript_unwrap_parenthesized_expression(expression) { + JavascriptExpression::BooleanLiteral(literal) => Some(if literal.value { + JavascriptLiteralValueKind::Truthy + } else { + JavascriptLiteralValueKind::Falsy + }), + JavascriptExpression::NullLiteral(_) => Some(JavascriptLiteralValueKind::Nullish), + JavascriptExpression::NumericLiteral(literal) => { + Some(if literal.value == 0.0 || literal.value.is_nan() { + JavascriptLiteralValueKind::Falsy + } else { + JavascriptLiteralValueKind::Truthy + }) + } + JavascriptExpression::StringLiteral(literal) => Some(if literal.value.is_empty() { + JavascriptLiteralValueKind::Falsy + } else { + JavascriptLiteralValueKind::Truthy + }), + JavascriptExpression::SequenceExpression(sequence) => sequence + .expressions + .last() + .and_then(javascript_literal_value_kind), + _ => None, + } +} + +fn javascript_expression_is_known_native_promise( + expression: &JavascriptExpression<'_>, + scoping: &JavascriptScoping, + known_promise_symbols: &BTreeSet, +) -> bool { + let expression = javascript_unwrap_parenthesized_expression(expression); + match expression { + JavascriptExpression::ImportExpression(_) => true, + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| scoping.get_reference(reference_id).symbol_id()) + .is_some_and(|symbol_id| known_promise_symbols.contains(&symbol_id)), + JavascriptExpression::NewExpression(expression) => { + matches!( + javascript_unwrap_parenthesized_expression(&expression.callee), + JavascriptExpression::Identifier(identifier) + if javascript_identifier_is_unresolved_known_global( + identifier, + scoping, + &["Promise"], + ) + ) + } + JavascriptExpression::CallExpression(call) => { + let Some(member) = + javascript_unwrap_parenthesized_expression(&call.callee).as_member_expression() + else { + return false; + }; + let Some(name) = member.static_property_name() else { + return false; + }; + if matches!( + name, + "all" | "allSettled" | "any" | "race" | "reject" | "resolve" + ) { + return matches!( + javascript_unwrap_parenthesized_expression(member.object()), + JavascriptExpression::Identifier(identifier) + if javascript_identifier_is_unresolved_known_global( + identifier, + scoping, + &["Promise"], + ) + ); + } + matches!(name, "then" | "catch" | "finally") + && javascript_expression_is_known_native_promise( + member.object(), + scoping, + known_promise_symbols, + ) + } + JavascriptExpression::AssignmentExpression(assignment) + if assignment.operator.is_assign() => + { + javascript_expression_is_known_native_promise( + &assignment.right, + scoping, + known_promise_symbols, + ) + } + JavascriptExpression::SequenceExpression(sequence) => { + sequence.expressions.last().is_some_and(|expression| { + javascript_expression_is_known_native_promise( + expression, + scoping, + known_promise_symbols, + ) + }) + } + JavascriptExpression::ConditionalExpression(conditional) => { + javascript_expression_is_known_native_promise( + &conditional.consequent, + scoping, + known_promise_symbols, + ) && javascript_expression_is_known_native_promise( + &conditional.alternate, + scoping, + known_promise_symbols, + ) + } + _ => false, + } +} + +#[derive(Default)] +struct JavascriptKnownPromiseBindingCollector { + assignments: BTreeMap, +} + +impl JavascriptKnownPromiseBindingCollector { + fn record(&mut self, symbol_id: JavascriptSymbolId, known: bool) { + let state = self.assignments.entry(symbol_id).or_default(); + state.0 |= known; + state.1 |= !known; + } +} + +struct JavascriptKnownPromiseBindingPass<'a> { + scoping: &'a JavascriptScoping, + known: &'a BTreeSet, + collector: JavascriptKnownPromiseBindingCollector, +} + +impl<'a> VisitJavascript<'a> for JavascriptKnownPromiseBindingPass<'_> { + fn visit_variable_declarator(&mut self, declarator: &JavascriptVariableDeclarator<'a>) { + if let (Some(identifier), Some(initializer)) = ( + declarator.id.get_binding_identifier(), + declarator.init.as_ref(), + ) { + if let Some(symbol_id) = identifier.symbol_id.get() { + self.collector.record( + symbol_id, + javascript_expression_is_known_native_promise( + initializer, + self.scoping, + self.known, + ), + ); + } + } + oxc_ast_visit::walk::walk_variable_declarator(self, declarator); + } + + fn visit_assignment_expression(&mut self, assignment: &oxc_ast::ast::AssignmentExpression<'a>) { + if assignment.operator.is_assign() { + if let oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) = + &assignment.left + { + if let Some(symbol_id) = identifier + .reference_id + .get() + .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) + { + self.collector.record( + symbol_id, + javascript_expression_is_known_native_promise( + &assignment.right, + self.scoping, + self.known, + ), + ); + } + } + } + oxc_ast_visit::walk::walk_assignment_expression(self, assignment); + } +} + +fn javascript_known_native_promise_symbols( + program: &oxc_ast::ast::Program<'_>, + scoping: &JavascriptScoping, +) -> BTreeSet { + let mut known = BTreeSet::new(); + for _ in 0..16 { + let mut pass = JavascriptKnownPromiseBindingPass { + scoping, + known: &known, + collector: JavascriptKnownPromiseBindingCollector::default(), + }; + pass.visit_program(program); + let next = pass + .collector + .assignments + .into_iter() + .filter_map(|(symbol_id, (has_known, has_unknown))| { + (has_known && !has_unknown).then_some(symbol_id) + }) + .collect::>(); + if next == known { + break; + } + known = next; + } + known +} + fn javascript_expression_is_known_event_target( expression: &JavascriptExpression<'_>, scoping: &JavascriptScoping, @@ -2072,6 +2302,7 @@ fn javascript_expression_is_known_event_target( fn javascript_known_callback_argument_indices( call: &JavascriptCallExpression<'_>, scoping: &JavascriptScoping, + known_promise_symbols: &BTreeSet, ) -> &'static [usize] { const NONE: &[usize] = &[]; const FIRST: &[usize] = &[0]; @@ -2108,7 +2339,11 @@ fn javascript_known_callback_argument_indices( receiver = &parenthesized.expression; } if matches!(name, "catch" | "finally" | "then") { - return if javascript_expression_is_dynamic_import_promise_chain(receiver) { + return if javascript_expression_is_known_native_promise( + receiver, + scoping, + known_promise_symbols, + ) { if name == "then" { FIRST_AND_SECOND } else { @@ -2162,25 +2397,6 @@ fn javascript_known_callback_argument_indices( NONE } -fn javascript_expression_is_dynamic_import_promise_chain( - expression: &JavascriptExpression<'_>, -) -> bool { - let expression = javascript_unwrap_parenthesized_expression(expression); - match expression { - JavascriptExpression::ImportExpression(_) => true, - JavascriptExpression::CallExpression(call) => { - let callee = javascript_unwrap_parenthesized_expression(&call.callee); - callee.as_member_expression().is_some_and(|member| { - matches!( - member.static_property_name(), - Some("then" | "catch" | "finally") - ) && javascript_expression_is_dynamic_import_promise_chain(member.object()) - }) - } - _ => false, - } -} - fn javascript_known_callback_is_synchronous(call: &JavascriptCallExpression<'_>) -> bool { let mut callee = &call.callee; while let JavascriptExpression::ParenthesizedExpression(parenthesized) = callee { @@ -2785,11 +3001,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { &self, member: &oxc_ast::ast::MemberExpression<'_>, ) -> Option<(JavascriptSymbolId, String)> { - let identifier = member.object().get_identifier_reference()?; - let receiver = identifier - .reference_id - .get() - .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id())?; + let receiver = javascript_referenced_value_symbol(member.object(), self.scoping)?; Some((receiver, member.static_property_name()?.to_string())) } @@ -3005,14 +3217,32 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { &logical.left, logical.left.span().start as usize, ); - if logical.operator.is_or() || logical.operator.is_coalesce() { + let literal = javascript_literal_value_kind(&logical.left); + if logical.operator.is_or() { self.record_expression( &logical.left, logical.left.span().start as usize, invoke_at, synchronous, ); - if !left_is_definite { + if literal != Some(JavascriptLiteralValueKind::Truthy) && !left_is_definite { + self.record_expression( + &logical.right, + logical.right.span().start as usize, + invoke_at, + synchronous, + ); + } + } else if logical.operator.is_coalesce() { + self.record_expression( + &logical.left, + logical.left.span().start as usize, + invoke_at, + synchronous, + ); + if literal.is_none_or(|kind| kind == JavascriptLiteralValueKind::Nullish) + && !left_is_definite + { self.record_expression( &logical.right, logical.right.span().start as usize, @@ -3021,12 +3251,16 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { ); } } else { - self.record_expression( - &logical.right, - logical.right.span().start as usize, - invoke_at, - synchronous, - ); + if literal != Some(JavascriptLiteralValueKind::Falsy) + && literal != Some(JavascriptLiteralValueKind::Nullish) + { + self.record_expression( + &logical.right, + logical.right.span().start as usize, + invoke_at, + synchronous, + ); + } } } JavascriptExpression::AssignmentExpression(assignment) => { @@ -3072,7 +3306,9 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { synchronous, ); } - } else { + } else if assignment.operator.is_assign() + || assignment.operator == JavascriptAssignmentOperator::LogicalAnd + { self.record_expression( &assignment.right, assignment.right.span().start as usize, @@ -3110,7 +3346,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { } fn callback_argument_indices(&self, call: &JavascriptCallExpression<'_>) -> &'static [usize] { - javascript_known_callback_argument_indices(call, self.scoping) + javascript_known_callback_argument_indices(call, self.scoping, self.known_promise_symbols) } } @@ -3205,6 +3441,26 @@ impl<'a> VisitJavascript<'a> for JavascriptFunctionInvocationCollector<'a, '_> { ) }) .collect::>(); + if let JavascriptExpression::CallExpression(bind_call) = + javascript_unwrap_parenthesized_expression(&expression.callee) + { + if let Some(bind_member) = + javascript_unwrap_parenthesized_expression(&bind_call.callee).as_member_expression() + { + let intrinsic_bind_is_possible = bind_member.static_property_name() == Some("bind") + && self + .method_indices_for_member(bind_member, resolve_at) + .is_empty() + && self + .member_callable_override_indices(bind_member, resolve_at) + .is_none_or(|(_, conditional)| conditional); + if intrinsic_bind_is_possible { + indices.extend( + self.callable_indices_for_expression(bind_member.object(), resolve_at), + ); + } + } + } indices.sort_unstable(); indices.dedup(); self.push_invocations(indices, invoke_at, true); @@ -3352,6 +3608,8 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan receiver_owner_collector.visit_program(&parsed.program); let (receiver_owner_events, callable_events, member_callable_events, super_aliases) = receiver_owner_collector.finish(); + let known_promise_symbols = + javascript_known_native_promise_symbols(&parsed.program, semantic.semantic.scoping()); let mut context_ranges = definitions.ranges.clone(); for _ in 0..16 { for range in &mut definitions.ranges { @@ -3361,6 +3619,7 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan } let mut invocations = JavascriptFunctionInvocationCollector { scoping: semantic.semantic.scoping(), + known_promise_symbols: &known_promise_symbols, content, context_ranges: &context_ranges, literal_false_ranges: &literal_false_ranges, @@ -6603,6 +6862,7 @@ struct JavascriptDynamicImportUsageCollector<'a> { struct JavascriptImportMemberCallCollector<'a> { scoping: &'a JavascriptScoping, + known_promise_symbols: &'a BTreeSet, content: &'a str, ranges: &'a [NamedJavascriptFunctionRange], literal_false_ranges: &'a JavascriptLiteralFalseRangeIndex, @@ -6824,7 +7084,11 @@ impl<'a> VisitJavascript<'a> for JavascriptImportMemberCallCollector<'_> { } self.record(&local, path, position); } - for index in javascript_known_callback_argument_indices(call, self.scoping) { + for index in javascript_known_callback_argument_indices( + call, + self.scoping, + self.known_promise_symbols, + ) { if let Some(expression) = call .arguments .get(*index) @@ -7710,6 +7974,8 @@ pub(super) fn javascript_module_analysis( if !semantic.diagnostics.is_empty() { return None; } + let known_promise_symbols = + javascript_known_native_promise_symbols(&parsed.program, semantic.semantic.scoping()); let mut collector = JavascriptModuleAnalysisCollector::default(); collector.visit_program(&parsed.program); if !collector.analysis.synthetic_declarations.is_empty() { @@ -7912,6 +8178,7 @@ pub(super) fn javascript_module_analysis( .collect::>(); let mut import_member_calls = JavascriptImportMemberCallCollector { scoping: semantic.semantic.scoping(), + known_promise_symbols: &known_promise_symbols, content, ranges: &alias_ranges, literal_false_ranges: &alias_ranges.literal_false_ranges, @@ -13684,6 +13951,52 @@ mod javascript_projection_reachability_tests { } } + #[test] + fn promise_callbacks_require_a_known_native_promise_source() { + for content in [ + "function callback(){return import('./promise-variable-import.mjs')} const pending = import('./seed.mjs'); pending.then(callback);", + "function callback(){return import('./promise-variable-resolve.mjs')} let pending; pending = Promise.resolve(1); pending.catch(callback);", + "function callback(){return import('./promise-direct-resolve.mjs')} Promise.resolve(1).finally(callback);", + "function callback(){return import('./promise-chain.mjs')} function noop(){} const pending = Promise.reject('x'); pending.catch(noop).then(callback);", + "function callback(){return import('./promise-alias.mjs')} const pending = new Promise((resolve) => resolve()); const alias = pending; alias.then(callback);", + ] { + let ranges = named_javascript_function_ranges(content); + let callback = content + .find(".mjs')}") + .expect("find known Promise callback import"); + assert!( + javascript_position_is_reachable(content, &ranges, callback), + "callbacks registered on a known native Promise must be reachable: {content}", + ); + } + + for content in [ + "function callback(){return import('./business-thenable-decoy.mjs')} const service = { then(handler) { return handler; } }; service.then(callback);", + "function callback(){return import('./shadowed-promise-decoy.mjs')} const Promise = { resolve() { return { then(handler) { return handler; } }; } }; Promise.resolve().then(callback);", + "function callback(){return import('./unproven-promise-variable-decoy.mjs')} const pending = getBusinessThenable(); pending.finally(callback);", + "function callback(){return import('./overwritten-promise-decoy.mjs')} let pending = Promise.resolve(); pending = getBusinessThenable(); pending.then(callback);", + ] { + let ranges = named_javascript_function_ranges(content); + let callback = content + .find(".mjs')}") + .expect("find unproven thenable callback import"); + assert!( + !javascript_position_is_reachable(content, &ranges, callback), + "business thenables must not be promoted to native Promise callback APIs: {content}", + ); + } + + let imported = "import { fulfilled, rejected } from './callbacks.mjs'; const pending = Promise.resolve(); pending.then(fulfilled, rejected);"; + let analysis = javascript_module_analysis(imported, true) + .expect("analyze imported native Promise callbacks"); + for callback in ["fulfilled", "rejected"] { + assert!( + analysis.import_member_calls.contains_key(callback), + "Promise.then must build an imported callback demand for {callback}", + ); + } + } + #[test] fn controlled_callback_captures_after_earlier_arguments() { let content = "function original() { return import('./listener-before-argument.mjs'); } function replacement() { return import('./listener-after-argument.mjs'); } let listener = original; function swap() { listener = replacement; return 'click'; } document.addEventListener(swap(), listener);"; @@ -14053,6 +14366,130 @@ mod javascript_projection_reachability_tests { } } + #[test] + fn compound_assignment_results_are_not_callable_rhs_values() { + for operator in [ + "+=", "-=", "*=", "/=", "%=", "**=", "|=", "^=", "&=", "<<=", ">>=", ">>>=", + ] { + let content = format!( + "function oldRun() {{ return import('./compound-old-{operator}.mjs'); }} function nextRun() {{ return import('./compound-rhs-{operator}.mjs'); }} let run = oldRun; (run {operator} nextRun)();" + ); + let ranges = named_javascript_function_ranges(&content); + for source in ["./compound-old-", "./compound-rhs-"] { + let position = content.find(source).expect("find compound callee decoy"); + assert!( + !javascript_position_is_reachable(&content, &ranges, position), + "{operator} produces a primitive result and must not call either function operand", + ); + } + } + } + + #[test] + fn logical_callees_respect_literal_short_circuit_values() { + for (expression, source) in [ + ("true || decoy", "./literal-true-or-decoy.mjs"), + ("1 || decoy", "./literal-one-or-decoy.mjs"), + ("false && decoy", "./literal-false-and-decoy.mjs"), + ("0 && decoy", "./literal-zero-and-decoy.mjs"), + ("false ?? decoy", "./literal-false-nullish-decoy.mjs"), + ("'' ?? decoy", "./literal-string-nullish-decoy.mjs"), + ] { + let content = + format!("function decoy() {{ return import('{source}'); }} ({expression})();"); + let ranges = named_javascript_function_ranges(&content); + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content + .find(source) + .expect("find literal short-circuit decoy"), + ), + "literal short-circuiting must exclude the unexecuted RHS: {expression}", + ); + } + + for (expression, source) in [ + ("false || selected", "./literal-false-or-selected.mjs"), + ("0 || selected", "./literal-zero-or-selected.mjs"), + ("true && selected", "./literal-true-and-selected.mjs"), + ("1 && selected", "./literal-one-and-selected.mjs"), + ("null ?? selected", "./literal-nullish-selected.mjs"), + ] { + let content = + format!("function selected() {{ return import('{source}'); }} ({expression})();"); + let ranges = named_javascript_function_ranges(&content); + assert!( + javascript_position_is_reachable( + &content, + &ranges, + content + .find(source) + .expect("find literal selected callable"), + ), + "literal short-circuiting must retain the executed RHS: {expression}", + ); + } + } + + #[test] + fn sequence_receivers_preserve_function_object_overrides_and_deletions() { + for member in ["call", "apply", "bind"] { + let content = format!( + "function target() {{ return import('./sequence-intrinsic-decoy-{member}.mjs'); }} function overrideMethod() {{ return import('./sequence-override-{member}.mjs'); }} (0, target).{member} = overrideMethod; (0, target).{member}(null);" + ); + let ranges = named_javascript_function_ranges(&content); + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content + .find(&format!("./sequence-intrinsic-decoy-{member}.mjs")) + .unwrap(), + ), + "a sequence receiver's own {member} override must disable the intrinsic", + ); + assert!( + javascript_position_is_reachable( + &content, + &ranges, + content + .find(&format!("./sequence-override-{member}.mjs")) + .unwrap(), + ), + "a sequence receiver must preserve the function object's {member} override", + ); + + let invocation = if member == "bind" { + "const rebound = target.bind(null); rebound();".to_string() + } else { + format!("target.{member}(null);") + }; + let deleted = format!( + "function target() {{ return import('./sequence-delete-intrinsic-{member}.mjs'); }} function overrideMethod() {{ return import('./sequence-delete-override-{member}.mjs'); }} target.{member} = overrideMethod; delete (0, target).{member}; {invocation}" + ); + let ranges = named_javascript_function_ranges(&deleted); + assert!(javascript_position_is_reachable( + &deleted, + &ranges, + deleted + .find(&format!("./sequence-delete-intrinsic-{member}.mjs")) + .unwrap(), + )); + assert!( + !javascript_position_is_reachable( + &deleted, + &ranges, + deleted + .find(&format!("./sequence-delete-override-{member}.mjs")) + .unwrap(), + ), + "delete through a sequence receiver must restore Function.prototype.{member}", + ); + } + } + #[test] fn new_invokes_ordinary_function_constructors() { for content in [ @@ -14068,6 +14505,32 @@ mod javascript_projection_reachability_tests { } } + #[test] + fn new_directly_invokes_an_intrinsically_bound_function_constructor() { + let content = "function Game() { return import('./direct-bound-function-constructor.mjs'); } new (Game.bind(null))();"; + let ranges = named_javascript_function_ranges(content); + assert!(javascript_position_is_reachable( + content, + &ranges, + content + .find("./direct-bound-function-constructor.mjs") + .unwrap(), + )); + + let overridden = "function Game() { return import('./direct-bound-overridden-decoy.mjs'); } function businessBind() { return function Replacement() {}; } Game.bind = businessBind; new (Game.bind(null))();"; + let ranges = named_javascript_function_ranges(overridden); + assert!( + !javascript_position_is_reachable( + overridden, + &ranges, + overridden + .find("./direct-bound-overridden-decoy.mjs") + .unwrap(), + ), + "an own bind override must not manufacture an intrinsic bound constructor", + ); + } + #[test] fn new_resolves_assignment_conditional_and_bound_class_callees() { let assignment = "class Game { constructor() { return import('./new-assignment.mjs'); } } let Constructor; new (Constructor = Game)();"; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d1e066810..e281766fd 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6007,6 +6007,7 @@ - 受控异步 callback 的 alias 读取按完整 enclosing invocation 链延迟到各层函数同步收尾,最外层再延迟到当前 job 末尾;callback 写入仍不在注册点同步提交。conditional / assignment expression callee 分别在 test / RHS 求值后建立调用边,`new` 同时执行普通 function constructor 及 alias。 - 函数对象自有 `.call / .apply / .bind` 覆盖允许以普通对象静态 member callable 作为 RHS,并继续按函数对象身份跨普通 alias 共享。`delete` 自有覆盖后恢复 Function.prototype intrinsic;条件删除合并覆盖与 intrinsic,非 callable 自有值仍视为属性存在并禁止 intrinsic 回退。 +- callable 运算结果只把普通 `=` 与实际执行分支的 `||= / &&= / ??=` 视为可调用值来源,算术、位运算和移位复合赋值不得直接采用 RHS callable。logical callee 对 boolean / numeric / string / null 字面量先执行真实短路;sequence receiver 的末项保持函数对象身份,用于 `.call / .apply / .bind` own override 与 `delete`。Promise callback 只接受固定 dynamic import、未遮蔽原生 `Promise` 构造 / 静态方法及可证明变量、alias 和连续链,业务 thenable、未知返回值和遮蔽 `Promise` 保持普通成员调用。直接 `new (Fn.bind(...))()` 在 intrinsic bind 仍可能时继承普通 function `Fn` 的 construct target,确定 own override 时不回退。 - ESM 最终绑定与 occurrence 补充:外部 callable root 只认模块初始化完成时同一 live binding / member 的最后一次直接顶层 assignment,旧 RHS 不得加载;直接 awaited namespace member 在赋值、受控 callback、constructor 和深层静态 member path 中仍按 `(source, occurrence)` 传播并替换首段 export。assignment class expression 的 static / instance member demand 分离。 - ESM 初始化与阻塞补充:projection declaration 保持原模块源码顺序,dependency origin 保持 importer 声明顺序;循环 canonical 去重同时删除声明和对应初始化写入。side-effect static import 只联结按 dependency-before-importer 排列的直接 `globalThis / window` 顶层 effect,不暴露 dependency local binding;静态可判定永不完成的 top-level await 至少对未遮蔽全局 `await new Promise(() => {})` 失败关闭,嵌套函数同形 decoy 和可完成 await 保持允许。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index e14318d65..0be4deb28 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -842,6 +842,7 @@ game-project/ - conditional callable 的 consequent / alternate 必须在 test 求值之后分别按自身表达式起点冻结身份;受控 callback 参数也按该参数自身起点解析,使更早参数的 alias 副作用先行生效,同时保持真实 callback 执行边位于注册调用之后。 - 异步 callback 的延迟状态必须沿完整 enclosing invocation 链传播:每层闭包读取该函数同步收尾后的局部 alias,越过最外层调用后再读取当前 job 末尾的外层 alias;不能只完成注册函数一层后,又在其调用点冻结更外层状态。conditional expression callee 在 test 完成后解析两支,assignment expression callee 直接调用 RHS 求值结果;`new` 除 class constructor 外也必须调用普通 function 及其 alias。函数对象自有 `.call / .apply / .bind` assignment 的 RHS 可来自普通对象静态 member callable;`delete` 通过任一共享函数 alias 删除这些自有覆盖后恢复 Function.prototype intrinsic,未知条件删除保留覆盖与 intrinsic 两种可能。删除态必须和“自有属性仍存在但值不可调用”分离,后者继续禁止 intrinsic 回退。 - callee 求值必须覆盖 sequence、logical expression 与 `||= / &&= / ??=`:sequence 只调用最后一项但保留前项副作用,logical 与 logical assignment 按 callable 的真值 / nullish 事实执行短路,未知状态才合并可运行分支,不能统一退化为 RHS 或把已跳过 RHS 的 decoy 建边。`new` 的 assignment / conditional callee 同样先完成 callee 求值,再冻结 constructor owner;未被 class 自有静态 `bind` 或函数对象 own override 覆盖的 Function.prototype `.bind()` 保留原 class construct target。`delete (((target.call)))` 等任意括号包装必须删除同一函数对象 own `.call/.apply/.bind` 并恢复 intrinsic。固定 dynamic import 返回的 Promise 可沿任意连续 `.then/.catch/.finally` 链传播 callback 可达性,链中每个 `.then` 只取 fulfilled / rejected 两槽,`.catch/.finally` 只取首槽,其余参数保持普通值。 +- callable 结果判定必须服从运算符真实返回值:普通 `=` 才直接采用 RHS callable,`+= / -= / *= / /= / %= / **= / <<= / >>= / >>>= / &= / ^= / |=` 的结果按 primitive 处理,不能把仅作为值参与运算的 RHS 函数误建成调用边。logical callee 先按 boolean / numeric / string / null 字面量执行 `|| / && / ??` 短路,已跳过的 RHS 不可达;sequence receiver 的最后一项继续保留原函数对象身份,使 `(0, target).call` 一类访问、覆盖安装和 `delete` 都命中同一个 own property。原生 Promise callback 允许从固定 dynamic import、未遮蔽全局 `new Promise`、`Promise.resolve / reject / all / allSettled / any / race` 及其可证明变量 / alias / 连续链建立边;任意业务 thenable、未知返回值和局部遮蔽的 `Promise` 不得仅凭 `.then/.catch/.finally` 同名升级为受控 API。`new (Fn.bind(...))()` 在未被业务静态成员或函数对象 own `bind` 覆盖时,必须直接保留普通 function `Fn` 的 construct target。 - 泥点不足是确定性业务中断,不是瞬态 Provider 故障或未知副作用。钱包的 `泥点余额不足` 与 `可消费泥点不足:...` 两种领域文案统一映射为稳定原因 `mud-points-insufficient`,不得自动重试;即使 External Generation durable ledger 已存在,也必须落为 `failed`,不能误入 `needs-reconciliation`。game-chat 顶部状态、持久失败对话与 `【Supervisor 阶段记录】` 统一显示“泥点余额不足,本轮游戏生成已中断。请充值后发送“继续”,系统会从当前项目进度接着完成。”,并禁止透传 operationId、URL、路径、密钥或任意上游正文。 - tool-plan 成功响应落账前,对内置 Runtime 原生函数与 legacy wrapper 的合法、无重复 key JSON arguments 按工具 schema 的精确位置做项目路径 canonicalization:`file.*.path`、`project.patchset.changes[*].path`、`project.git_commit.paths[*]`、`command.*.cwd`、`image.inspect.paths[*]` 与 `canvas.asset_generate.outputPath` 若是当前项目根目录内的完整绝对路径,转换为 `/` 分隔的项目相对路径后再校验、持久化并执行;源码/叙述字段、任务产物描述、动态 MCP arguments 和项目外绝对路径不得改写,后两者继续由绝对路径门禁失败关闭。项目根只允许搜索/列举范围与命令 cwd 规范化为 `.`,不能成为文件目标。当前进程与重启恢复都必须从同一份规范化 handoff 重放,禁止分别执行原响应和持久响应。