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 0b3fd5a4c..92bed62a4 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 @@ -2,9 +2,9 @@ use super::*; use oxc_allocator::Allocator as JavascriptAllocator; use oxc_ast::ast::{ Argument as JavascriptArgument, ArrowFunctionExpression as JavascriptArrowFunctionExpression, - BindingPattern as JavascriptBindingPattern, BindingProperty as JavascriptBindingProperty, - CallExpression as JavascriptCallExpression, Class as JavascriptClass, - ComputedMemberExpression as JavascriptComputedMemberExpression, + AssignmentOperator as JavascriptAssignmentOperator, BindingPattern as JavascriptBindingPattern, + BindingProperty as JavascriptBindingProperty, CallExpression as JavascriptCallExpression, + Class as JavascriptClass, ComputedMemberExpression as JavascriptComputedMemberExpression, Declaration as JavascriptDeclaration, ExportAllDeclaration as JavascriptExportAllDeclaration, ExportDeclaration as JavascriptExportDeclaration, ExportDefaultDeclarationKind as JavascriptExportDefaultDeclarationKind, @@ -1391,6 +1391,10 @@ enum JavascriptReceiverOwnerValue { force_instance: bool, frozen_at: usize, }, + BoundClass { + source: JavascriptSymbolId, + captured_at: usize, + }, } #[derive(Clone)] @@ -1427,6 +1431,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { &self, expression: &JavascriptExpression<'_>, ) -> Option { + let expression = javascript_unwrap_parenthesized_expression(expression); let identifier = expression.get_identifier_reference()?; identifier .reference_id @@ -1439,41 +1444,9 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { symbol_id: JavascriptSymbolId, initializer: &JavascriptExpression<'_>, position: usize, + force_conditional: bool, ) { - let values = match initializer { - JavascriptExpression::ObjectExpression(object) => { - vec![JavascriptReceiverOwnerValue::Direct(( - object.span.start as usize, - object.span.end as usize, - false, - ))] - } - JavascriptExpression::ClassExpression(class) => { - vec![JavascriptReceiverOwnerValue::Direct(( - class.span.start as usize, - class.span.end as usize, - true, - ))] - } - JavascriptExpression::NewExpression(new_expression) => { - self.new_expression_receiver_values(new_expression) - } - JavascriptExpression::Identifier(_) => self - .referenced_symbol(initializer) - .map(|source| JavascriptReceiverOwnerValue::Alias { - source, - force_instance: false, - frozen_at: position, - }) - .into_iter() - .collect(), - JavascriptExpression::ConditionalExpression(expression) => { - let mut values = self.receiver_values(&expression.consequent, position); - values.extend(self.receiver_values(&expression.alternate, position)); - values - } - _ => Vec::new(), - }; + let values = self.receiver_values(initializer, position); if javascript_position_is_in_literal_false_block( self.content, self.literal_false_ranges, @@ -1482,8 +1455,8 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { return; } let events = self.owner_events.entry(symbol_id).or_default(); - let conditional = - javascript_position_is_conditionally_executed(self.conditional_ranges, position); + let conditional = force_conditional + || javascript_position_is_conditionally_executed(self.conditional_ranges, position); if values.is_empty() { events.push(JavascriptAliasEvent { position, @@ -1598,6 +1571,13 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { force_instance: true, frozen_at, }, + JavascriptReceiverOwnerValue::BoundClass { + source, + captured_at, + } => JavascriptReceiverOwnerValue::BoundClass { + source, + captured_at, + }, }) .collect() } @@ -1625,6 +1605,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { receiver_captured_at: usize, captured_at: usize, position: usize, + force_conditional: bool, ) { if javascript_position_is_in_literal_false_block( self.content, @@ -1634,8 +1615,8 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { return; } let values = self.callable_values(expression, captured_at); - let conditional = - javascript_position_is_conditionally_executed(self.conditional_ranges, position); + let conditional = force_conditional + || javascript_position_is_conditionally_executed(self.conditional_ranges, position); let events = self .member_callable_events .entry((receiver, name)) @@ -1711,6 +1692,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { expression: &JavascriptExpression<'_>, captured_at: usize, position: usize, + force_conditional: bool, ) { if javascript_position_is_in_literal_false_block( self.content, @@ -1726,20 +1708,22 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { position, scope: javascript_alias_scope_at(self.ranges, position), value: None, - conditional: javascript_position_is_conditionally_executed( - self.conditional_ranges, - position, - ), + conditional: force_conditional + || javascript_position_is_conditionally_executed( + self.conditional_ranges, + position, + ), }); } else { events.extend(values.into_iter().map(|value| JavascriptAliasEvent { position, scope: javascript_alias_scope_at(self.ranges, position), value: Some(value), - conditional: javascript_position_is_conditionally_executed( - self.conditional_ranges, - position, - ), + conditional: force_conditional + || javascript_position_is_conditionally_executed( + self.conditional_ranges, + position, + ), })); } } @@ -1784,10 +1768,86 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { values.extend(self.receiver_values(&expression.alternate, position)); values } + JavascriptExpression::CallExpression(call) => call + .callee + .as_member_expression() + .filter(|member| member.static_property_name() == Some("bind")) + .and_then(|member| self.referenced_symbol(member.object())) + .map(|source| JavascriptReceiverOwnerValue::BoundClass { + source, + captured_at: call.span.start as usize, + }) + .into_iter() + .collect(), _ => Vec::new(), } } + fn callable_value_is_definite( + &self, + value: &JavascriptCallableValue, + visiting: &mut BTreeSet, + ) -> bool { + match value { + JavascriptCallableValue::Direct(_) => true, + JavascriptCallableValue::Alias { + source, + captured_at, + } => self.symbol_is_definitely_callable_at(*source, *captured_at, visiting), + JavascriptCallableValue::Bound { + source, + captured_at, + .. + } => self.symbol_is_definitely_callable_at(*source, *captured_at, visiting), + JavascriptCallableValue::Member { .. } + | JavascriptCallableValue::NonCallableMember + | JavascriptCallableValue::DeletedMember => false, + } + } + + fn symbol_is_definitely_callable_at( + &self, + symbol_id: JavascriptSymbolId, + at: usize, + visiting: &mut BTreeSet, + ) -> bool { + if !visiting.insert(symbol_id) { + return false; + } + let binding_start = self.scoping.symbol_span(symbol_id).start as usize; + let base_is_callable = self + .ranges + .iter() + .any(|range| range.binding_start == Some(binding_start)); + let result = + self.callable_events + .get(&symbol_id) + .map_or(base_is_callable, |events| { + let selection = javascript_alias_event_indices_at( + events, + self.ranges, + self.content, + self.literal_false_ranges, + self.conditional_ranges, + at, + &mut BTreeSet::new(), + false, + ); + if !selection.resolved { + return base_is_callable; + } + let selected_are_callable = !selection.indices.is_empty() + && selection.indices.iter().all(|index| { + events[*index].value.as_ref().is_some_and(|value| { + self.callable_value_is_definite(value, visiting) + }) + }); + selected_are_callable && (!selection.conditional || base_is_callable) + }); + visiting.remove(&symbol_id); + result + } + fn finish( self, ) -> ( @@ -1845,6 +1905,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { symbol_id, initializer, declarator.span.end as usize, + false, ); if !matches!( initializer, @@ -1859,6 +1920,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { initializer, initializer.span().start as usize, declarator.span.end as usize, + false, ); } } @@ -1867,7 +1929,30 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { } fn visit_assignment_expression(&mut self, assignment: &oxc_ast::ast::AssignmentExpression<'a>) { - if assignment.operator.is_assign() { + if assignment.operator.is_assign() || assignment.operator.is_logical() { + let target_symbol = match &assignment.left { + oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => { + identifier.reference_id.get().and_then(|reference_id| { + self.scoping.get_reference(reference_id).symbol_id() + }) + } + _ => None, + }; + let target_is_callable = target_symbol.is_some_and(|symbol_id| { + self.symbol_is_definitely_callable_at( + symbol_id, + assignment.span.start as usize, + &mut BTreeSet::new(), + ) + }); + let assignment_runs = assignment.operator.is_assign() + || assignment.operator == JavascriptAssignmentOperator::LogicalAnd + || !target_is_callable; + let force_conditional = assignment.operator.is_logical() && !target_is_callable; + if !assignment_runs { + oxc_ast_visit::walk::walk_assignment_expression(self, assignment); + return; + } if let Some((receiver, name)) = self.assignment_member_key(&assignment.left) { self.record_member_callable_assignment( receiver, @@ -1876,6 +1961,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { assignment.span.start as usize, assignment.right.span().start as usize, assignment.span.end as usize, + assignment.operator.is_logical(), ); } if let oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) = @@ -1890,12 +1976,14 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { symbol_id, &assignment.right, assignment.span.end as usize, + force_conditional, ); self.record_callable_assignment( symbol_id, &assignment.right, assignment.right.span().start as usize, assignment.span.end as usize, + force_conditional, ); } } @@ -1905,7 +1993,9 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { fn visit_unary_expression(&mut self, expression: &JavascriptUnaryExpression<'a>) { if expression.operator == JavascriptUnaryOperator::Delete { - if let Some(member) = expression.argument.as_member_expression() { + if let Some(member) = javascript_unwrap_parenthesized_expression(&expression.argument) + .as_member_expression() + { if let Some((receiver, name)) = self .referenced_symbol(member.object()) .zip(member.static_property_name().map(str::to_string)) @@ -1943,6 +2033,15 @@ struct JavascriptFunctionInvocationCollector<'a, 'b> { marker: std::marker::PhantomData<&'a ()>, } +fn javascript_unwrap_parenthesized_expression<'a, 'b>( + mut expression: &'b JavascriptExpression<'a>, +) -> &'b JavascriptExpression<'a> { + while let JavascriptExpression::ParenthesizedExpression(parenthesized) = expression { + expression = &parenthesized.expression; + } + expression +} + fn javascript_identifier_is_unresolved_known_global( identifier: &oxc_ast::ast::IdentifierReference<'_>, scoping: &JavascriptScoping, @@ -2028,7 +2127,7 @@ fn javascript_known_callback_argument_indices( receiver = &parenthesized.expression; } if matches!(name, "catch" | "finally" | "then") { - return if matches!(receiver, JavascriptExpression::ImportExpression(_)) { + return if javascript_expression_is_dynamic_import_promise_chain(receiver) { if name == "then" { FIRST_AND_SECOND } else { @@ -2082,6 +2181,25 @@ 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 { @@ -2244,6 +2362,107 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { indices } + fn callable_value_is_definite( + &self, + value: &JavascriptCallableValue, + visiting: &mut BTreeSet, + ) -> bool { + match value { + JavascriptCallableValue::Direct(_) => true, + JavascriptCallableValue::Alias { + source, + captured_at, + } => self.callable_symbol_is_definite(*source, *captured_at, visiting), + JavascriptCallableValue::Bound { + source, + captured_at, + .. + } => { + self.callable_symbol_is_definite(*source, *captured_at, visiting) + && self + .member_callable_override_indices_for_key( + &(*source, "bind".to_string()), + *captured_at, + ) + .is_none() + } + JavascriptCallableValue::Member { .. } + | JavascriptCallableValue::NonCallableMember + | JavascriptCallableValue::DeletedMember => false, + } + } + + fn callable_symbol_is_definite( + &self, + symbol_id: JavascriptSymbolId, + at: usize, + visiting: &mut BTreeSet, + ) -> bool { + if !visiting.insert(symbol_id) { + return false; + } + let binding_start = self.scoping.symbol_span(symbol_id).start as usize; + let base_is_callable = self.binding_ranges.contains_key(&binding_start); + let result = + self.callable_events + .get(&symbol_id) + .map_or(base_is_callable, |events| { + let selection = javascript_alias_event_indices_at( + events, + self.context_ranges, + self.content, + self.literal_false_ranges, + self.conditional_ranges, + at, + &mut BTreeSet::new(), + false, + ); + if !selection.resolved { + return base_is_callable; + } + let selected_are_callable = !selection.indices.is_empty() + && selection.indices.iter().all(|index| { + events[*index].value.as_ref().is_some_and(|value| { + self.callable_value_is_definite(value, visiting) + }) + }); + selected_are_callable && (!selection.conditional || base_is_callable) + }); + visiting.remove(&symbol_id); + result + } + + fn expression_is_definitely_callable( + &self, + expression: &JavascriptExpression<'_>, + at: usize, + ) -> bool { + match expression { + JavascriptExpression::Identifier(identifier) => identifier + .reference_id + .get() + .and_then(|reference_id| self.scoping.get_reference(reference_id).symbol_id()) + .is_some_and(|symbol_id| { + self.callable_symbol_is_definite(symbol_id, at, &mut BTreeSet::new()) + }), + JavascriptExpression::ParenthesizedExpression(parenthesized) => { + self.expression_is_definitely_callable(&parenthesized.expression, at) + } + JavascriptExpression::FunctionExpression(_) + | JavascriptExpression::ArrowFunctionExpression(_) => true, + JavascriptExpression::ConditionalExpression(conditional) => { + self.expression_is_definitely_callable( + &conditional.consequent, + conditional.consequent.span().start as usize, + ) && self.expression_is_definitely_callable( + &conditional.alternate, + conditional.alternate.span().start as usize, + ) + } + _ => false, + } + } + fn callable_indices_for_expression( &self, expression: &JavascriptExpression<'_>, @@ -2352,6 +2571,28 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { (start, end, if *force_instance { false } else { is_static }) }), ), + JavascriptReceiverOwnerValue::BoundClass { + source, + captured_at, + } => { + let source_owners = + self.receiver_owners_for_symbol(*source, *captured_at, visiting); + let has_business_bind = source_owners.iter().any(|owner| { + !self + .method_indices_for_owner(*owner, "bind", &mut BTreeSet::new()) + .is_empty() + }); + let intrinsic_possible = self + .member_callable_override_indices_for_key( + &(*source, "bind".to_string()), + *captured_at, + ) + .is_none_or(|(_, conditional)| conditional) + && !has_business_bind; + if intrinsic_possible { + owners.extend(source_owners); + } + } } } } @@ -2409,6 +2650,86 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { .into_iter() .map(|(start, end, _)| (start, end, false)) .collect(), + JavascriptExpression::ConditionalExpression(conditional) => { + let mut owners = self.receiver_owner( + &conditional.consequent, + conditional.consequent.span().start as usize, + ); + owners.extend(self.receiver_owner( + &conditional.alternate, + conditional.alternate.span().start as usize, + )); + owners.sort_unstable(); + owners.dedup(); + owners + } + JavascriptExpression::AssignmentExpression(assignment) => { + if matches!( + assignment.operator, + JavascriptAssignmentOperator::LogicalOr + | JavascriptAssignmentOperator::LogicalNullish + ) { + let left_owners = match &assignment.left { + oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => { + identifier + .reference_id + .get() + .and_then(|reference_id| { + self.scoping.get_reference(reference_id).symbol_id() + }) + .map(|symbol_id| { + self.receiver_owners_for_symbol( + symbol_id, + assignment.span.start as usize, + &mut BTreeSet::new(), + ) + }) + .unwrap_or_default() + } + _ => Vec::new(), + }; + if !left_owners.is_empty() { + left_owners + } else { + self.receiver_owner( + &assignment.right, + assignment.right.span().start as usize, + ) + } + } else { + self.receiver_owner(&assignment.right, assignment.right.span().start as usize) + } + } + JavascriptExpression::SequenceExpression(sequence) => sequence + .expressions + .last() + .map(|expression| self.receiver_owner(expression, expression.span().start as usize)) + .unwrap_or_default(), + JavascriptExpression::LogicalExpression(logical) => { + let left = self.receiver_owner(&logical.left, logical.left.span().start as usize); + if !left.is_empty() && (logical.operator.is_or() || logical.operator.is_coalesce()) + { + left + } else { + self.receiver_owner(&logical.right, logical.right.span().start as usize) + } + } + JavascriptExpression::CallExpression(call) => { + let Some(member) = call.callee.as_member_expression() else { + return Vec::new(); + }; + if member.static_property_name() != Some("bind") + || !self + .method_indices_for_member(member, call.span.start as usize) + .is_empty() + || self + .member_callable_override_indices(member, call.span.start as usize) + .is_some_and(|(_, conditional)| !conditional) + { + return Vec::new(); + } + self.receiver_owner(member.object(), call.span.start as usize) + } _ => Vec::new(), } } @@ -2688,13 +3009,96 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { synchronous, ); } - JavascriptExpression::AssignmentExpression(assignment) => { - self.record_expression( - &assignment.right, - assignment.right.span().start as usize, - invoke_at, - synchronous, + JavascriptExpression::SequenceExpression(sequence) => { + if let Some(last) = sequence.expressions.last() { + self.record_expression( + last, + last.span().start as usize, + invoke_at, + synchronous, + ); + } + } + JavascriptExpression::LogicalExpression(logical) => { + let left_is_definite = self.expression_is_definitely_callable( + &logical.left, + logical.left.span().start as usize, ); + if logical.operator.is_or() || logical.operator.is_coalesce() { + self.record_expression( + &logical.left, + logical.left.span().start as usize, + invoke_at, + synchronous, + ); + if !left_is_definite { + self.record_expression( + &logical.right, + logical.right.span().start as usize, + invoke_at, + synchronous, + ); + } + } else { + self.record_expression( + &logical.right, + logical.right.span().start as usize, + invoke_at, + synchronous, + ); + } + } + JavascriptExpression::AssignmentExpression(assignment) => { + let target = match &assignment.left { + oxc_ast::ast::AssignmentTarget::AssignmentTargetIdentifier(identifier) => { + Some(identifier) + } + _ => None, + }; + let target_is_definite = target.is_some_and(|identifier| { + identifier + .reference_id + .get() + .and_then(|reference_id| { + self.scoping.get_reference(reference_id).symbol_id() + }) + .is_some_and(|symbol_id| { + self.callable_symbol_is_definite( + symbol_id, + assignment.span.start as usize, + &mut BTreeSet::new(), + ) + }) + }); + if matches!( + assignment.operator, + JavascriptAssignmentOperator::LogicalOr + | JavascriptAssignmentOperator::LogicalNullish + ) { + if let Some(identifier) = target { + self.record_identifier( + identifier, + assignment.span.start as usize, + invoke_at, + synchronous, + ); + } + if !target_is_definite { + self.record_expression( + &assignment.right, + assignment.right.span().start as usize, + invoke_at, + synchronous, + ); + } + } else { + self.record_expression( + &assignment.right, + assignment.right.span().start as usize, + invoke_at, + synchronous, + ); + } } JavascriptExpression::FunctionExpression(_) | JavascriptExpression::ArrowFunctionExpression(_) => { @@ -12626,6 +13030,69 @@ mod javascript_projection_reachability_tests { )); } + #[test] + fn logical_assignment_and_sequence_callees_preserve_short_circuit_state() { + for (operator, expected, rejected) in [ + ("||=", "./logical-or-old.mjs", "./logical-or-next.mjs"), + ("&&=", "./logical-and-next.mjs", "./logical-and-old.mjs"), + ( + "??=", + "./logical-nullish-old.mjs", + "./logical-nullish-next.mjs", + ), + ] { + let content = format!( + "function oldRun() {{ return import('{old}'); }} function nextRun() {{ return import('{next}'); }} let run = oldRun; (run {operator} nextRun)();", + old = if operator == "&&=" { rejected } else { expected }, + next = if operator == "&&=" { expected } else { rejected }, + ); + let ranges = named_javascript_function_ranges(&content); + assert!( + javascript_position_is_reachable( + &content, + &ranges, + content + .find(expected) + .expect("find expected logical target"), + ), + "{operator} must call its runtime result", + ); + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content + .find(rejected) + .expect("find rejected logical target"), + ), + "{operator} must preserve callable short-circuit semantics", + ); + } + + for (label, callee) in [ + ("sequence", "(swap(), run)"), + ("logical", "(swap() && run)"), + ] { + let content = format!( + "function oldRun() {{ return import('./{label}-old.mjs'); }} function nextRun() {{ return import('./{label}-next.mjs'); }} let run = oldRun; function swap() {{ run = nextRun; return true; }} {callee}();" + ); + let ranges = named_javascript_function_ranges(&content); + assert!(javascript_position_is_reachable( + &content, + &ranges, + content.find(&format!("./{label}-next.mjs")).unwrap(), + )); + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content.find(&format!("./{label}-old.mjs")).unwrap(), + ), + "{label} callee must resolve after earlier expression side effects", + ); + } + } + #[test] fn new_invokes_ordinary_function_constructors() { for content in [ @@ -12641,6 +13108,47 @@ mod javascript_projection_reachability_tests { } } + #[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)();"; + let conditional = "class First { constructor() { return import('./new-conditional-first.mjs'); } } class Second { constructor() { return import('./new-conditional-second.mjs'); } } new (flag ? First : Second)();"; + let bound = "class Game { constructor() { return import('./new-bound-class.mjs'); } } const BoundGame = Game.bind(null); new BoundGame();"; + for (content, expected) in [ + (assignment, vec!["./new-assignment.mjs"]), + ( + conditional, + vec![ + "./new-conditional-first.mjs", + "./new-conditional-second.mjs", + ], + ), + (bound, vec!["./new-bound-class.mjs"]), + ] { + let ranges = named_javascript_function_ranges(content); + for source in expected { + assert!( + javascript_position_is_reachable( + content, + &ranges, + content.find(source).expect("find constructor import"), + ), + "new must retain the construct target for {source}", + ); + } + } + + let business_bind = "class Game { constructor() { return import('./new-business-bind-decoy.mjs'); } static bind() { return class {}; } } const BoundGame = Game.bind(null); new BoundGame();"; + let ranges = named_javascript_function_ranges(business_bind); + assert!( + !javascript_position_is_reachable( + business_bind, + &ranges, + business_bind.find("./new-business-bind-decoy.mjs").unwrap(), + ), + "a class-owned bind method must not be mistaken for Function.prototype.bind", + ); + } + #[test] fn deleting_function_object_overrides_restores_intrinsics() { for member in ["call", "apply", "bind"] { @@ -12705,6 +13213,38 @@ mod javascript_projection_reachability_tests { ); } + #[test] + fn parenthesized_delete_restores_function_prototype_intrinsics() { + for member in ["call", "apply", "bind"] { + let invocation = if member == "bind" { + "const bound = target.bind(null); bound();".to_string() + } else { + format!("target.{member}(null);") + }; + let content = format!( + "function target() {{ return import('./parenthesized-delete-intrinsic-{member}.mjs'); }} function overrideMethod() {{ return import('./parenthesized-delete-override-{member}.mjs'); }} target.{member} = overrideMethod; delete (((target.{member}))); {invocation}" + ); + let ranges = named_javascript_function_ranges(&content); + assert!(javascript_position_is_reachable( + &content, + &ranges, + content + .find(&format!("./parenthesized-delete-intrinsic-{member}.mjs")) + .unwrap(), + )); + assert!( + !javascript_position_is_reachable( + &content, + &ranges, + content + .find(&format!("./parenthesized-delete-override-{member}.mjs")) + .unwrap(), + ), + "parenthesized delete must remove the own {member} override", + ); + } + } + #[test] fn class_expression_instance_owner_survives_new_assignment() { let content = "const game = new (class { run() { return import('./class-expression-instance.mjs'); } })(); game.run();"; @@ -12760,6 +13300,45 @@ mod javascript_projection_reachability_tests { } } + #[test] + fn chained_dynamic_import_promises_execute_each_exact_callback_slot() { + let content = "function fulfilled(){return import('./chain-fulfilled.mjs')} function rejected(){return import('./chain-rejected.mjs')} function caught(){return import('./chain-caught.mjs')} function catchExtra(){return import('./chain-catch-extra.mjs')} function settled(){return import('./chain-finally.mjs')} function finallyExtra(){return import('./chain-finally-extra.mjs')} function next(){return import('./chain-next.mjs')} import('./seed.mjs').then(fulfilled, rejected).catch(caught, catchExtra).finally(settled, finallyExtra).then(next);"; + assert_callback_reachability( + content, + &[ + "./chain-fulfilled.mjs", + "./chain-rejected.mjs", + "./chain-caught.mjs", + "./chain-finally.mjs", + "./chain-next.mjs", + ], + &["./chain-catch-extra.mjs", "./chain-finally-extra.mjs"], + ); + + let imported = "import * as api from './api.mjs'; import('./seed.mjs').then(api.fulfilled, api.rejected).catch(api.caught, api.catchExtra).finally(api.settled, api.finallyExtra).then(api.next);"; + let analysis = javascript_module_analysis(imported, true).expect("analyze Promise chain"); + let members = analysis + .import_member_calls + .get("api") + .expect("record Promise chain callback demands"); + for callback in ["fulfilled", "rejected", "caught", "settled", "next"] { + assert!( + members + .keys() + .any(|path| path.first().is_some_and(|member| member == callback)), + "missing chained Promise callback demand for {callback}: {members:?}", + ); + } + for extra in ["catchExtra", "finallyExtra"] { + assert!( + !members + .keys() + .any(|path| path.first().is_some_and(|member| member == extra)), + "extra Promise argument must remain a value: {extra}", + ); + } + } + #[test] fn import_member_callback_demands_use_the_same_exact_slots() { let content = "import * as api from './api.mjs'; setTimeout(api.timer, api.delay); document.addEventListener(api.eventType, api.listener, api.options); import('./seed.mjs').then(api.fulfilled, api.rejected); [1].reduce(api.reducer, api.initialValue); setTIMEOUT(api.wrongCase); [1].foreach(api.wrongMethodCase);"; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c84cb8519..cd2713af8 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5993,6 +5993,7 @@ - JavaScript / ESM callee 与终止顺序补充:identifier callee 和 `new C(args)` 的 constructor / instance owner 在实参前冻结,invocation effect 保留在实参之后;callable assignment 到 RHS 完成后才生效,`start = start()` 继续调用旧值。`return / throw` 表达式中的 assignment / call 先执行,截断点取表达式之后的 AST statement end;函数体使用 Oxc body span,不从默认参数或解构参数中的首个 `{` 猜测。未知 guard clause 后续与 `catch` 体一律按 conditional effect 合并旧状态。 - JavaScript callable 分支与参数快照补充:conditional expression 必须在 test 求值完成后,分别于 consequent / alternate 自身起点冻结 callable identity;受控 callback 参数按该参数自身起点解析,前置参数产生的 alias 副作用先于后续 callback identity 生效,callback 的执行边仍保留在注册调用完成位置。 - JavaScript / ESM live binding 投影补充:被选 export root 的直接顶层 assignment 及其 RHS 依赖必须与原声明共同投影,覆盖 `export let x; x = impl`、导出对象成员安装和 class prototype 安装;assignment target 以 semantic root symbol 归属,函数体写入、嵌套控制流和无关 root 写入不得因同名文本进入投影。共享 declaration 的写入按原始源码位置合并,继续参与 canonical 重命名、循环去重和既有 `2 MiB / 32 MiB` 门禁;全部依赖声明必须先于延后的初始化写入输出,不能因 projection traversal 产生 TDZ。 +- JavaScript callee 短路、构造与 Promise 链补充:sequence callee 保留前序求值副作用并只调用末项,logical expression 及 `||= / &&= / ??=` 按已知 callable 真值 / nullish 状态短路,未知状态才保留可运行分支。`new` 支持 assignment / conditional callee,并让未被 class 静态业务成员或函数对象 own override 覆盖的 `.bind()` 结果继续指向原 class construct target。`delete` 对 `.call/.apply/.bind` 的括号包装不改变 own-property identity,删除后恢复 Function.prototype intrinsic。dynamic import Promise 的连续 `.then/.catch/.finally` 任意深度都建立 callback 边,但参数槽固定为 `then=[0,1]`、`catch/finally=[0]`,额外参数不得升级为执行 demand。 - JavaScript callback 时序、内建覆盖与 class expression owner 补充:受控异步 callback 的注册位置只建立可达调用边,闭包读取的外层 alias 状态选取注册所在同步作用域收尾点,不能冻结在注册点;函数体写副作用仍不得同步提交到注册调用末尾。数组字面量上的已知迭代 callback 继续按同步执行传播外层 alias 变化。函数对象自有 `.call / .apply / .bind` assignment 按函数对象身份形成成员 callable 状态,普通函数别名共享同一对象覆盖,`.bind()` 结果保持独立对象身份;存在覆盖时禁止回退到 Function.prototype 语义。`new (class { ... })` 赋给局部变量时冻结 class expression 的 instance owner,使后续实例方法调用保持可达。 - JavaScript / ESM assignment root 与直接动态 namespace 补充:被选 export 的顶层 live-binding function / arrow assignment、对象成员安装和 class prototype 安装同时成为对应 root / member 的 projected reachability root,class static 与 instance assignment 不得串线;`(await import('./dep.mjs')).run()` 及等价静态 computed member 直接记录 occurrence-scoped `run` export demand,恒假分支、未调用函数与其它既有可达性边界继续生效。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index b7d83f09a..e67160f3e 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -839,6 +839,7 @@ game-project/ - `new` 必须覆盖显式 `super()`、隐式 derived constructor 和直接 class expression;class expression 经 `new` 赋给局部变量时必须冻结 instance owner,未 override 的 instance / static 方法沿冻结的 `extends` 链查找。普通嵌套 function 拥有独立 `this`,只有 arrow 词法继承外层 owner。声明后通过 assignment 安装的 function / arrow、本地 function alias 和 `.bind()` 结果都必须保留 callable identity;只有未被函数对象自有成员覆盖的 Function.prototype `.call()` / `.apply()` / `.bind()` 采用内建 callable 语义,自有覆盖和业务对象同名方法都按实际成员 callable 解析。函数对象自有 `.call / .apply / .bind` 覆盖必须按对象身份传播到全部普通别名,`.bind()` 生成的新函数则保持独立对象身份。callable conditional expression 必须合并 consequent / alternate 的全部可调用身份;声明式和后续赋值式 `api.run.bind(api)` 都在绑定时冻结成员 owner 与方法名。受控 callback 识别穿透 callee 与 receiver 外层括号;Promise `.then` 的 fulfilled / rejected 两个参数都是 callback,`.catch` / `.finally` 仍仅取第一个。timer、RAF、microtask、Promise 与事件监听 callback 的注册点只建立异步可达调用边,闭包读取外层 alias 时必须选取当前同步作用域收尾后的状态,不能冻结在注册点;callback 函数体对外层 alias 的写副作用仍不得同步提交到注册调用末尾。数组字面量的已知迭代 API 才按同步 callback 传播函数体副作用。视觉资产启发式允许使用 ASCII 小写副本查找路径、`drawImage` 与图片变量,但函数调用图、semantic binding 和可达性必须始终解析原始大小写 JavaScript,禁止把 `MainLoop` 与 `mainloop` 合并。Canvas 视觉门必须复用同一组受 256 文件、累计源码 `2 MiB` 与投影处理 `32 MiB` 限制的 inline / external module 加载、链接和投影结果,并逐个原始大小写 unit 独立关联资产路径、图片变量与可达 `drawImage`;`