feat(vm): PC 轨迹对拍工具 + 2 处语义修复(定位到首个执行流分歧)

做了什么:
- 新增 tools/tdc_trace.js: 给分发器 M=G[z[H++]]() 打补丁, dump 真实执行 PC 序列
- tdc_vm_run.py: 新增 TRACE_FULL 全量 PC 轨迹; 子 VM 共享 pcs/events(轨迹是全局的)
- 修复 CALL_FUNC 的 this 绑定: 真实语义 fn.apply(c, B) -> this = c (原来传 None)
- DBGCALL 调试输出(callee 类型/closure 异常/ref 与栈尾)

为什么:
- 单靠读语义会无限试错; PC 轨迹对拍能一步定位首个分歧

怎么验证:
- node tools/tdc_trace.js tcaptcha/core_js/tdc_sample.js  -> 真实 19266 步
- 我们的解释器 739 步; 逐条 diff 后:**前 713 步完全一致**
- 首个分歧 @step714: real 进入 @1544, 我们 fall-through 到 @208
  对应指令 @206 CALL_METHOD [4]; 我们的 ref=[None,'call'] (base 为 None),
  真实应为一个可调用对象 -> 说明某个寄存器在我们这边是 None
This commit is contained in:
yml2213
2026-10-11 22:04:44 +08:00
parent 1fbd8f46f5
commit 3f91b0a4e3
2 changed files with 68 additions and 2 deletions
+20 -2
View File
@@ -226,6 +226,8 @@ class VM:
self.ret = None
self.key_hits: List[Any] = []
self.trace = bool(os.environ.get("TRACE"))
self.trace_full = bool(os.environ.get("TRACE_FULL"))
self.pcs: List[int] = [] # 全量 PC 轨迹 (TRACE_FULL 时记录)
self.events: List[Any] = [] # 记录调用/闭包/返回等关键事件
# ---- 工具
@@ -250,6 +252,8 @@ class VM:
if self.steps > self.max_steps:
raise RuntimeError("step limit exceeded")
op = z[self.H]; self.H += 1
if self.trace_full:
self.pcs.append(self.H - 1)
if self.trace and op in (1, 6, 19, 34, 41, 49, 57, 61):
self.events.append((self.H - 1, op))
h = self.OP.get(op)
@@ -313,7 +317,9 @@ class VM:
args = E[-n:] if n else []
del E[len(E) - n:]
fn = E.pop()
E.append(self.call(fn, args, None))
if os.environ.get("DBGCALL"):
print(f"[CALL_FUNC @{self.H-2}] n={n} callee={type(fn).__name__} {str(fn)[:70]}", file=sys.stderr)
E.append(self.call(fn, args, self.c)) # 真实语义: fn.apply(c, B) -> this = c
@op(8) # GET_GLOBAL_PROP
def _8():
@@ -393,6 +399,10 @@ class VM:
args = E[-n:] if n else []
del E[len(E) - n:]
ref = E.pop()
if os.environ.get("DBGCALL"):
callee = getprop(ref[0], ref[1])
print(f"[CALL_METHOD @{self.H-2}] n={n} ref={str(ref)[:90]} arity/type={type(ref).__name__}", file=sys.stderr)
print(f" stack tail: {[str(x)[:36] for x in E[-6:]]}", file=sys.stderr)
E.append(self.call(getprop(ref[0], ref[1]), args, ref[0]))
@op(37) # JUMP_IF_TRUE
@@ -536,6 +546,10 @@ class VM:
if isinstance(fn, tuple) and fn and fn[0] == "closure":
sub = VM(self.z, self.c, self.max_steps - self.steps)
sub.key_hits = self.key_hits # 子 VM 共享命中列表
sub.pcs = self.pcs # 共享 PC 轨迹 (轨迹是全局的)
sub.events = self.events # 共享关键事件
sub.trace = self.trace
sub.trace_full = self.trace_full
sub.E = [JArr() for _ in range(32)]
sub.E[0] = JArr([this]); sub.E[1] = JArr([JArr(args)]); sub.E[2] = JArr([fn])
for (q, r) in fn[2]:
@@ -547,7 +561,11 @@ class VM:
v = sub._run()
self.steps += sub.steps
return v
except Exception:
except Exception as exc:
if os.environ.get("DBGCALL"):
import traceback
print(f"[closure @{fn[1]} 执行异常] {type(exc).__name__}: {exc}", file=sys.stderr)
traceback.print_exc(limit=3)
return JS_UNDEF
if isinstance(fn, HostFn):
return fn(*args)