修复异步回调状态与函数别名覆盖

区分异步回调的可达注册点与闭包外层状态执行点
按函数对象身份共享 call、apply、bind 自有成员覆盖
补充异步时序、函数别名和独立 bind 对象回归
同步技术方案与项目决策记录
This commit is contained in:
2026-08-04 14:42:18 +08:00
parent 88d3de2398
commit 9250fb7b1f
3 changed files with 253 additions and 20 deletions
@@ -1083,6 +1083,8 @@ struct NamedJavascriptFunctionRange {
member_static: bool,
externally_callable: bool,
invocations: Vec<usize>,
// (registration/call site used for reachability, execution point used for outer state reads)
invocation_state_positions: Vec<(usize, usize)>,
synchronous_invocations: Vec<usize>,
}
@@ -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<JavascriptSymbolId, Vec<JavascriptAliasEvent<JavascriptCallableValue>>>,
member_callable_events:
BTreeMap<(JavascriptSymbolId, String), Vec<JavascriptAliasEvent<JavascriptCallableValue>>>,
BTreeMap<(JavascriptSymbolId, String), Vec<JavascriptMemberCallableEvent>>,
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<JavascriptCallableValue>,
}
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<JavascriptSymbolId, Vec<JavascriptAliasEvent<JavascriptReceiverOwnerValue>>>,
BTreeMap<JavascriptSymbolId, Vec<JavascriptAliasEvent<JavascriptCallableValue>>>,
BTreeMap<(JavascriptSymbolId, String), Vec<JavascriptAliasEvent<JavascriptCallableValue>>>,
BTreeMap<(JavascriptSymbolId, String), Vec<JavascriptMemberCallableEvent>>,
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<JavascriptSymbolId, Vec<JavascriptAliasEvent<JavascriptCallableValue>>>,
member_callable_events:
BTreeMap<(JavascriptSymbolId, String), Vec<JavascriptAliasEvent<JavascriptCallableValue>>>,
BTreeMap<(JavascriptSymbolId, String), Vec<JavascriptMemberCallableEvent>>,
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<usize>, 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<JavascriptSymbolId>,
) -> 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<JavascriptSymbolId>,
) -> 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::<BTreeSet<_>>();
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<usize>, 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::<Vec<_>>();
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<T>(
.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<T>(
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]
@@ -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,使后续实例方法调用保持可达。
@@ -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 后续重赋值不能反向改写 receiveridentifier 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 / instanceclass 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 expressionclass 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``<script type="module">`、本地 `<script type="module" src>` 及其真实可达依赖可以形成证据,未链接文件、恒假动态依赖、未调用函数、跨 unit 拼接和仅出现在 HTML 文本中的路径不得借用其它 unit 的绘制调用过门。
- `new` 必须覆盖显式 `super()`、隐式 derived constructor 和直接 class expressionclass 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``<script type="module">`、本地 `<script type="module" src>` 及其真实可达依赖可以形成证据,未链接文件、恒假动态依赖、未调用函数、跨 unit 拼接和仅出现在 HTML 文本中的路径不得借用其它 unit 的绘制调用过门。
- conditional callable 的 consequent / alternate 必须在 test 求值之后分别按自身表达式起点冻结身份;受控 callback 参数也按该参数自身起点解析,使更早参数的 alias 副作用先行生效,同时保持真实 callback 执行边位于注册调用之后。
- 泥点不足是确定性业务中断,不是瞬态 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 重放,禁止分别执行原响应和持久响应。