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 0f1677fba..66146eace 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 @@ -1083,6 +1083,8 @@ struct NamedJavascriptFunctionRange { member_static: bool, externally_callable: bool, invocations: Vec, + // (registration/call site used for reachability, execution point used for outer state reads) + invocation_state_positions: Vec<(usize, usize)>, synchronous_invocations: Vec, } @@ -1150,6 +1152,7 @@ impl JavascriptFunctionDefinitionCollector { member_static, externally_callable: false, invocations: Vec::new(), + invocation_state_positions: Vec::new(), synchronous_invocations: Vec::new(), }); } @@ -1343,7 +1346,7 @@ struct JavascriptMethodReceiverOwnerCollector<'a> { callable_events: BTreeMap>>, member_callable_events: - BTreeMap<(JavascriptSymbolId, String), Vec>>, + BTreeMap<(JavascriptSymbolId, String), Vec>, super_aliases: Vec<((usize, usize), JavascriptSymbolId, usize)>, } @@ -1372,9 +1375,17 @@ enum JavascriptCallableValue { Bound { source: JavascriptSymbolId, captured_at: usize, + object_span: (usize, usize), }, } +#[derive(Clone)] +struct JavascriptMemberCallableEvent { + // Member receiver evaluation precedes the assignment RHS and must keep that object identity. + receiver_captured_at: usize, + event: JavascriptAliasEvent, +} + impl JavascriptMethodReceiverOwnerCollector<'_> { fn referenced_symbol( &self, @@ -1500,6 +1511,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { return vec![JavascriptCallableValue::Bound { source, captured_at, + object_span: (call.span.start as usize, call.span.end as usize), }]; } bind.object() @@ -1562,6 +1574,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { receiver: JavascriptSymbolId, name: String, expression: &JavascriptExpression<'_>, + receiver_captured_at: usize, captured_at: usize, position: usize, ) { @@ -1580,19 +1593,29 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { .entry((receiver, name)) .or_default(); if values.is_empty() { - events.push(JavascriptAliasEvent { - position, - scope: javascript_alias_scope_at(self.ranges, position), - value: None, - conditional, + events.push(JavascriptMemberCallableEvent { + receiver_captured_at, + event: JavascriptAliasEvent { + position, + scope: javascript_alias_scope_at(self.ranges, position), + value: None, + conditional, + }, }); } else { - events.extend(values.into_iter().map(|value| JavascriptAliasEvent { - position, - scope: javascript_alias_scope_at(self.ranges, position), - value: Some(value), - conditional, - })); + events.extend( + values + .into_iter() + .map(|value| JavascriptMemberCallableEvent { + receiver_captured_at, + event: JavascriptAliasEvent { + position, + scope: javascript_alias_scope_at(self.ranges, position), + value: Some(value), + conditional, + }, + }), + ); } } @@ -1684,7 +1707,7 @@ impl JavascriptMethodReceiverOwnerCollector<'_> { ) -> ( BTreeMap>>, BTreeMap>>, - BTreeMap<(JavascriptSymbolId, String), Vec>>, + BTreeMap<(JavascriptSymbolId, String), Vec>, Vec<((usize, usize), JavascriptSymbolId, usize)>, ) { let super_owners = self.super_aliases.into_iter().collect(); @@ -1764,6 +1787,7 @@ impl<'a> VisitJavascript<'a> for JavascriptMethodReceiverOwnerCollector<'_> { receiver, name, &assignment.right, + assignment.span.start as usize, assignment.right.span().start as usize, assignment.span.end as usize, ); @@ -1809,7 +1833,7 @@ struct JavascriptFunctionInvocationCollector<'a, 'b> { callable_events: BTreeMap>>, member_callable_events: - BTreeMap<(JavascriptSymbolId, String), Vec>>, + BTreeMap<(JavascriptSymbolId, String), Vec>, super_aliases: Vec<((usize, usize), JavascriptSymbolId, usize)>, marker: std::marker::PhantomData<&'a ()>, } @@ -1982,9 +2006,28 @@ fn javascript_invocation_effect_position(span: oxc_span::Span) -> usize { } impl JavascriptFunctionInvocationCollector<'_, '_> { + fn asynchronous_state_position(&self, registration_at: usize) -> usize { + // Browser callbacks run after the current synchronous job/scope completes. Keeping this + // separate from the registration call site prevents both registration-time closure + // freezing and synchronous propagation of callback writes. + self.context_ranges + .iter() + .filter(|range| range.start <= registration_at && registration_at < range.end) + .min_by_key(|range| range.end - range.start) + .map_or(self.content.len(), |range| range.end.saturating_sub(1)) + } + fn push_invocations(&mut self, indices: Vec, at: usize, synchronous: bool) { + let state_at = if synchronous { + at + } else { + self.asynchronous_state_position(at) + }; for index in indices { self.ranges[index].invocations.push(at); + self.ranges[index] + .invocation_state_positions + .push((at, state_at)); if synchronous { self.ranges[index].synchronous_invocations.push(at); } @@ -2020,6 +2063,7 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { JavascriptCallableValue::Bound { source, captured_at, + .. } => { let override_is_conditional_or_absent = self .member_callable_override_indices_for_key( @@ -2326,6 +2370,76 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { Some((receiver, member.static_property_name()?.to_string())) } + fn callable_object_identities_for_value( + &self, + value: &JavascriptCallableValue, + visiting: &mut BTreeSet, + ) -> BTreeSet<(usize, usize)> { + match value { + JavascriptCallableValue::Direct(span) => [*span].into_iter().collect(), + JavascriptCallableValue::Alias { + source, + captured_at, + } => self.callable_object_identities_for_symbol(*source, *captured_at, visiting), + JavascriptCallableValue::Member { + receiver, + name, + captured_at, + } => self + .receiver_owners_for_symbol(*receiver, *captured_at, &mut BTreeSet::new()) + .into_iter() + .flat_map(|owner| self.method_indices_for_owner(owner, name, &mut BTreeSet::new())) + .map(|index| (self.ranges[index].start, self.ranges[index].end)) + .collect(), + JavascriptCallableValue::Bound { object_span, .. } => { + [*object_span].into_iter().collect() + } + } + } + + fn callable_object_identities_for_symbol( + &self, + symbol_id: JavascriptSymbolId, + at: usize, + visiting: &mut BTreeSet, + ) -> BTreeSet<(usize, usize)> { + if !visiting.insert(symbol_id) { + return BTreeSet::new(); + } + let binding_start = self.scoping.symbol_span(symbol_id).start as usize; + let mut identities = self + .binding_ranges + .get(&binding_start) + .into_iter() + .flatten() + .map(|index| (self.ranges[*index].start, self.ranges[*index].end)) + .collect::>(); + if let Some(events) = self.callable_events.get(&symbol_id) { + let selection = javascript_alias_event_indices_at( + events, + self.context_ranges, + self.content, + self.literal_false_ranges, + self.conditional_ranges, + at, + &mut BTreeSet::new(), + ); + if selection.resolved { + if !selection.conditional { + identities.clear(); + } + for index in selection.indices { + if let Some(value) = events[index].value.as_ref() { + identities + .extend(self.callable_object_identities_for_value(value, visiting)); + } + } + } + } + visiting.remove(&symbol_id); + identities + } + fn member_callable_override_indices( &self, member: &oxc_ast::ast::MemberExpression<'_>, @@ -2340,9 +2454,34 @@ impl JavascriptFunctionInvocationCollector<'_, '_> { key: &(JavascriptSymbolId, String), at: usize, ) -> Option<(Vec, bool)> { - let events = self.member_callable_events.get(key)?; + let receiver_identities = + self.callable_object_identities_for_symbol(key.0, at, &mut BTreeSet::new()); + let events = self + .member_callable_events + .iter() + .filter(|((_, name), _)| name == &key.1) + .flat_map(|((receiver, _), events)| { + events.iter().filter_map(|member_event| { + let matches_receiver = if receiver_identities.is_empty() { + *receiver == key.0 + } else { + self.callable_object_identities_for_symbol( + *receiver, + member_event.receiver_captured_at, + &mut BTreeSet::new(), + ) + .iter() + .any(|identity| receiver_identities.contains(identity)) + }; + matches_receiver.then(|| member_event.event.clone()) + }) + }) + .collect::>(); + if events.is_empty() { + return None; + } let selection = javascript_alias_event_indices_at( - events, + &events, self.context_ranges, self.content, self.literal_false_ranges, @@ -2671,6 +2810,7 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan for _ in 0..16 { for range in &mut definitions.ranges { range.invocations.clear(); + range.invocation_state_positions.clear(); range.synchronous_invocations.clear(); } let mut invocations = JavascriptFunctionInvocationCollector { @@ -2693,6 +2833,8 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan for range in &mut definitions.ranges { range.invocations.sort_unstable(); range.invocations.dedup(); + range.invocation_state_positions.sort_unstable(); + range.invocation_state_positions.dedup(); range.synchronous_invocations.sort_unstable(); range.synchronous_invocations.dedup(); } @@ -2702,6 +2844,7 @@ fn named_javascript_function_ranges(content: &str) -> NamedJavascriptFunctionRan .zip(&context_ranges) .all(|(current, previous)| { current.invocations == previous.invocations + && current.invocation_state_positions == previous.invocation_state_positions && current.synchronous_invocations == previous.synchronous_invocations }); context_ranges = definitions.ranges.clone(); @@ -5478,7 +5621,7 @@ fn javascript_alias_event_indices_at( .iter() .find(|range| range.start == scope.0 && range.end == scope.1) { - for invocation in &range.invocations { + for (invocation, state_at) in &range.invocation_state_positions { if (range.start..range.end).contains(invocation) || javascript_position_is_in_literal_false_block( content, @@ -5494,7 +5637,7 @@ fn javascript_alias_event_indices_at( content, literal_false_ranges, conditional_ranges, - *invocation, + *state_at, visiting, ); state.indices.extend(outer.indices); @@ -11139,6 +11282,31 @@ mod javascript_projection_reachability_tests { )); } + #[test] + fn asynchronous_callbacks_read_outer_aliases_after_registration() { + for content in [ + "function oldRun() { return import('./async-before-registration.mjs'); } function nextRun() { return import('./async-after-registration.mjs'); } let run = oldRun; function callback() { run(); } setTimeout(callback, 0); run = nextRun;", + "function oldRun() { return import('./nested-async-before-registration.mjs'); } function nextRun() { return import('./nested-async-after-registration.mjs'); } function setup() { let run = oldRun; function callback() { run(); } setTimeout(callback, 0); run = nextRun; } setup();", + ] { + let ranges = named_javascript_function_ranges(content); + let before = content + .find("before-registration.mjs") + .expect("find pre-registration callable"); + let after = content + .find("after-registration.mjs") + .expect("find post-registration callable"); + + assert!( + !javascript_position_is_reachable(content, &ranges, before), + "an asynchronous callback must not freeze an outer alias at registration", + ); + assert!( + javascript_position_is_reachable(content, &ranges, after), + "an asynchronous callback must observe the outer alias after synchronous setup finishes", + ); + } + } + #[test] fn function_object_overrides_disable_call_apply_and_bind_intrinsics() { for member in ["call", "apply", "bind"] { @@ -11192,6 +11360,71 @@ mod javascript_projection_reachability_tests { "a conditional own override must preserve both possible call targets: {source}", ); } + + for member in ["call", "apply", "bind"] { + for (installation, invocation) in [ + ( + format!("alias.{member} = overrideMethod;"), + format!("target.{member}(null);"), + ), + ( + format!("target.{member} = overrideMethod;"), + format!("alias.{member}(null);"), + ), + ] { + let content = format!( + "function target() {{ return import('./aliased-intrinsic-decoy-{member}.mjs'); }} function overrideMethod() {{ return import('./aliased-override-{member}.mjs'); }} const alias = target; {installation} {invocation}" + ); + let ranges = named_javascript_function_ranges(&content); + let decoy = content + .find(&format!("./aliased-intrinsic-decoy-{member}.mjs")) + .expect("find aliased intrinsic decoy import"); + let override_import = content + .find(&format!("./aliased-override-{member}.mjs")) + .expect("find aliased function-object override import"); + + assert!( + !javascript_position_is_reachable(&content, &ranges, decoy), + "an own {member} override installed through an alias must disable the intrinsic", + ); + assert!( + javascript_position_is_reachable(&content, &ranges, override_import), + "function aliases must share the same own {member} override", + ); + } + } + + let bound_identity = "function target() { return import('./bound-object-target.mjs'); } function overrideMethod() { return import('./bound-object-override.mjs'); } const bound = target.bind(null); bound.call = overrideMethod; target.call(null);"; + let ranges = named_javascript_function_ranges(bound_identity); + assert!(javascript_position_is_reachable( + bound_identity, + &ranges, + bound_identity.find("./bound-object-target.mjs").unwrap(), + )); + assert!( + !javascript_position_is_reachable( + bound_identity, + &ranges, + bound_identity.find("./bound-object-override.mjs").unwrap(), + ), + "a bound function must retain an object identity independent from its source", + ); + + let late_override = "function target() { return import('./before-alias-override.mjs'); } function overrideMethod() { return import('./late-alias-override.mjs'); } const alias = target; target.call(null); alias.call = overrideMethod;"; + let ranges = named_javascript_function_ranges(late_override); + assert!(javascript_position_is_reachable( + late_override, + &ranges, + late_override.find("./before-alias-override.mjs").unwrap(), + )); + assert!( + !javascript_position_is_reachable( + late_override, + &ranges, + late_override.find("./late-alias-override.mjs").unwrap(), + ), + "a later override through an alias must not rewrite an earlier intrinsic call", + ); } #[test] diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index ed12ddeae..22f431e8f 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5993,4 +5993,4 @@ - 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 callback 时序、内建覆盖与 class expression owner 补充:受控异步 callback 的注册位置只作为可达根和闭包状态捕获点,不再作为函数体副作用的同步提交点;数组字面量上的已知迭代 callback 继续按同步执行传播外层 alias 变化。函数对象自有 `.call / .apply / .bind` assignment 形成成员 callable 状态,存在覆盖时禁止回退到 Function.prototype 语义;`new (class { ... })` 赋给局部变量时冻结 class expression 的 instance owner,使后续实例方法调用保持可达。 +- JavaScript callback 时序、内建覆盖与 class expression owner 补充:受控异步 callback 的注册位置只建立可达调用边,闭包读取的外层 alias 状态选取注册所在同步作用域收尾点,不能冻结在注册点;函数体写副作用仍不得同步提交到注册调用末尾。数组字面量上的已知迭代 callback 继续按同步执行传播外层 alias 变化。函数对象自有 `.call / .apply / .bind` assignment 按函数对象身份形成成员 callable 状态,普通函数别名共享同一对象覆盖,`.bind()` 结果保持独立对象身份;存在覆盖时禁止回退到 Function.prototype 语义。`new (class { ... })` 赋给局部变量时冻结 class expression 的 instance owner,使后续实例方法调用保持可达。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index a215b9c62..0548f2826 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -836,7 +836,7 @@ game-project/ - dynamic import demand 必须以 source 与 import occurrence position 联合作为身份,同 source 的可达裸 import 不得继承不可达 occurrence 的 export。动态 namespace 在首段 export 之后继续保留完整 member path,声明 initializer 与后续赋值式 `await import` 都必须绑定 semantic symbol。named / namespace 成员作为回调参数、对象解构 alias、实例 alias 与下游 wrapper 时继续按真实调用 span 传播 demand。constructor、`new Game().method()` 和实例 alias 分别形成精确成员根,`this.method()` 只匹配相同 class / object owner;顶层声明位置只使用 Oxc span,禁止以源码文本 `find` 反查。受控 callback API 名严格大小写敏感,调用图只读取真实 callback 参数位:timer、microtask、RAF、Promise 与数组迭代 API 取第一个参数,`addEventListener` 取第二个参数,delay、initial value、event type、options 和其它额外参数都不得建立执行边。恒假分支区间由单次函数可达性 analysis 预计算、排序合并并借用二分索引,不得使用 thread-local 完整源码 key 或在每个位置查询时复制区间、重扫整份源码。 - imported member 只有作为受控 callback API 的参数时才建立执行 demand;日志、注册元数据等普通传参不能被当作调用。普通 member alias 必须同时支持声明 initializer 与后续赋值,`new ns.Game()` 的完整 constructor path 继续传播到实例 alias。局部对象、class、实例、`this` 和 `super` 的方法调用统一按 semantic receiver owner 匹配,禁止按末级方法名跨 owner 选中同名 decoy。 - member alias、局部 receiver 与动态 namespace 的 assignment 必须保留赋值位置和 enclosing function scope;函数体内的使用必须按真实 invocation position 选择当时已生效的事件,多次调用跨过赋值边界时合并全部可能 owner,恒假分支、未调用函数或调用之后的赋值不能倒灌覆盖更早使用点。`receiver = source` 必须冻结赋值完成时的 source owner,source 后续重赋值不能反向改写 receiver;identifier callee 与 `new C(args)` 的 constructor / instance owner 在参数求值前冻结,真实 invocation effect 仍在参数求值后生效,callable assignment 只在 RHS 完成后替换旧值,因此 `start = start()` 必须调用旧 callable。`return / throw` 的表达式先完整执行,随后才截断后续事件;函数体和截断位置统一使用 Oxc AST body / statement span,不能被默认参数或解构参数中的 `{}` 误导。未知 guard clause 后的语句与 `catch` 体赋值都保留执行和跳过状态;调用事件继续按 JavaScript 的 callee、参数 / RHS、内层调用、外层调用 / 赋值顺序生效,`outer(inner())` 与 `receiver = mutate()` 不得按 AST 先序颠倒副作用。未知条件、循环以及 `switch case/default` 内赋值保留执行与跳过状态,conditional expression 保留全部可解析 owner,无法解析的确定重赋值显式 invalidation;已调用函数对外层 alias 的最终副作用按调用位置回写,但普通函数体内无条件 `return / throw` 之后的事件必须截断。被导出的 function、class/object member 还必须把模块初始化完成视为潜在外部调用时点,使声明之后生效的顶层赋值进入 demand,同时保留此前本地调用的旧状态。动态依赖传播继续以原始 owner 模块 AST span 为权威,不得因投影重排声明或省略独立赋值语句而重算出相反 alias。class owner 必须继续区分 static / instance,class expression 与实例化 alias 进入相同 receiver 图,`super` 的 parent owner 在 class 定义位置冻结。constructor、`.call()` 与 `.apply()` 都是实际 invocation;具名 function expression 只保留外层 binding 对应的单一 range,未被受控 API 执行的 inline function / arrow 保持不可达。恒真 / 恒假关键字只按 JavaScript 大小写识别,局部或参数遮蔽的 `undefined` 不得当作恒假;恒假分支识别必须先用 parser trivia 屏蔽注释和非执行 literal。callback API 只接受 semantic 未解析的已知全局调度函数或 AST 可证明的 literal array / dynamic import 调用;用户定义、导入或遮蔽的同名 `setTimeout / map / then` 不能触发参数执行推断。 -- `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 解析。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 的副作用同步提交到注册调用末尾;数组字面量的已知迭代 API 才按同步 callback 传播函数体副作用。视觉资产启发式允许使用 ASCII 小写副本查找路径、`drawImage` 与图片变量,但函数调用图、semantic binding 和可达性必须始终解析原始大小写 JavaScript,禁止把 `MainLoop` 与 `mainloop` 合并。Canvas 视觉门必须复用同一组受 256 文件、累计源码 `2 MiB` 与投影处理 `32 MiB` 限制的 inline / external module 加载、链接和投影结果,并逐个原始大小写 unit 独立关联资产路径、图片变量与可达 `drawImage`;`