Files
TankIC/3DTank/tools/gen_slice_plan.js
T

506 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// gen_slice_plan.js —— 从 module_10032 抽取某个根状态切片的「状态字段 + reducer 分支表」
//
// 用途:为逐个迁移大切片(shop / user / screen / garage …)提供机器可读证据,
// 避免人工在 1 万行 reducer 里数位置。产出 3DTank/names/slice_<field>.json:
// State : 状态类(class / 构造参数 / 槽位 / toString 字段 / 默认值 / copy 方法)
// Copy : copy 方法的参数个数(== 字段数才认为可信)
// Branches : 每个顶层动作分支 { Local, Name, Kind, Changed:[{Field, Slot, Index, Value}], Expr }
// Kind = copy / reset / noop / conditional;Value 保留原表达式文本,供人工翻译
// SubReducers : 顶层分支里内嵌的 `(function (t, n) { … })(t, n.<子状态>)` 子 reducer
// (例如 shop 的 x4 分支会把 shopCategories/shopItems 两个嵌套模型交给子 reducer)
// { Var, Arg, Style(chain|ternary), ArmCount, Locals:[{Local,Name}], Arms:[{Local,Expr}] }
// Counts : TotalInstanceof / TopLevelBranches / SubReducerArms,用来说明「为什么不是一句话」
// Locals : 局部动作类 → 动作名(action_inventory 里查不到则为 null)
//
// 用法:cd 3DTank/tools && node gen_slice_plan.js shop [user screen ...]
'use strict';
const fs = require('fs');
const path = require('path');
const P = path.resolve(__dirname, '..');
const T = fs.readFileSync(path.join(P, 'src', 'module_10032.js'), 'utf8');
const Inv = JSON.parse(fs.readFileSync(path.join(P, 'names', 'action_inventory_10032.json'), 'utf8'));
const LocalToName = new Map();
for (const R of Inv.Rows) if (!LocalToName.has(R.Local)) LocalToName.set(R.Local, R.Name);
LocalToName.set('iz', 'InitState');
// ---------------------------------------------------------------- 基础扫描
function BracePairs(Text) {
const Pairs = new Map();
const Stack = [];
let I = 0;
while (I < Text.length) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C; I++;
while (I < Text.length) { if (Text[I] === '\\') { I += 2; continue; } if (Text[I] === Q) { I++; break; } I++; }
continue;
}
if (C === '/' && Text[I + 1] === '/') { const N = Text.indexOf('\n', I); I = N < 0 ? Text.length : N; continue; }
if (C === '/' && Text[I + 1] === '*') { const N = Text.indexOf('*/', I + 2); I = N < 0 ? Text.length : N + 2; continue; }
if (C === '{') Stack.push(I);
else if (C === '}') { const O = Stack.pop(); if (O !== undefined) Pairs.set(O, I); }
I++;
}
return Pairs;
}
const Pairs = BracePairs(T);
const Fns = new Map();
for (const M of T.matchAll(/(?<![\w$.])function ([A-Za-z0-9_$]+)\(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End !== undefined) Fns.set(M[1], { Name: M[1], Params: M[2], Body: Open + 1, End });
}
const Methods = [];
for (const M of T.matchAll(/\(([A-Za-z_$][\w$]*)\(([A-Za-z0-9_$]+)\)\.([A-Za-z0-9_$]+) = function \(([^)]*)\)\s*\{/g)) {
const Open = M.index + M[0].length - 1;
const End = Pairs.get(Open);
if (End !== undefined) Methods.push({ Obj: M[2], Method: M[3], Params: M[4], Start: M.index, Body: Open + 1, End });
}
function FindMethod(Obj, Method) { return Methods.find((M) => M.Obj === Obj && M.Method === Method) || null; }
// ---------------------------------------------------------------- 根 reducer / 根状态
const RootReducerFn = [...Fns.values()].find((F) => T.slice(F.Body, F.End).includes('return n.cti('));
if (!RootReducerFn) throw new Error('找不到根 reducer');
const CopyMethod = Methods.find((M) => M.Method === 'cti');
const RootClass = CopyMethod.Obj;
const RootClassFn = Fns.get(RootClass);
const RootBody = T.slice(RootClassFn.Body, RootClassFn.End);
const SlotOfParam = new Map();
for (const M of RootBody.matchAll(/\(this\.(\w+) = (\w+)\)/g)) SlotOfParam.set(M[2], M[1]);
const DefaultOfParam = new Map();
for (const M of RootBody.matchAll(/\((\w+) = \1 === cd \? ([\s\S]*?) : \1\)/g)) DefaultOfParam.set(M[1], M[2].replace(/\s+/g, ' ').trim());
for (const M of RootBody.matchAll(/\((\w+) = \1 !== cd && \1\)/g)) DefaultOfParam.set(M[1], 'false');
for (const M of RootBody.matchAll(/\((\w+) = \1 === cd \|\| \1\)/g)) DefaultOfParam.set(M[1], 'true');
const ToStringStart = T.indexOf(`(ld(${RootClass}).toString = function`);
const ToStringEnd = T.indexOf(`(ld(${RootClass}).hashCode`, ToStringStart);
const RootFields = [];
for (const M of T.slice(ToStringStart, ToStringEnd).matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
RootFields.push({ Field: M[2], Slot: M[3] });
}
/** 根 reducer 顶层赋值 [{Name, Expr}]。 */
function TopLevelAssignments(Body) {
const Out = [];
let I = 0, Depth = 0;
while (I < Body.length) {
const C = Body[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C; I++;
while (I < Body.length) { if (Body[I] === '\\') { I += 2; continue; } if (Body[I] === Q) { I++; break; } I++; }
continue;
}
if ('([{'.includes(C)) { Depth++; I++; continue; }
if (')]}'.includes(C)) { Depth--; I++; continue; }
if (Depth === 0) {
const M = /^([A-Za-z_$][\w$]*)\s*=\s*/.exec(Body.slice(I));
if (M && (I === 0 || /[;{}\s]/.test(Body[I - 1]))) {
let J = I + M[0].length, D = 0;
while (J < Body.length) {
const D0 = Body[J];
if (D0 === '"' || D0 === "'" || D0 === '`') {
const Q = D0; J++;
while (J < Body.length) { if (Body[J] === '\\') { J += 2; continue; } if (Body[J] === Q) { J++; break; } J++; }
continue;
}
if ('([{'.includes(D0)) D++;
else if (')]}'.includes(D0)) D--;
else if ((D0 === ',' || D0 === ';') && D === 0) break;
J++;
}
Out.push({ Name: M[1], Expr: Body.slice(I + M[0].length, J).trim() });
I = J + 1; continue;
}
}
I++;
}
return Out;
}
const Assignments = TopLevelAssignments(T.slice(RootReducerFn.Body, RootReducerFn.End));
// ---------------------------------------------------------------- 工具
/** 顶层逗号切分(按掩码文本判定深度,字符串里的逗号不切)。 */
function SplitTop(Text) {
const M = MaskStrings(Text);
const Out = []; let D = 0, Cur = '';
for (let I = 0; I < Text.length; I++) {
const C = M[I];
if ('([{'.includes(C)) D++;
else if (')]}'.includes(C)) D--;
if (C === ',' && D === 0) { Out.push(Cur.trim()); Cur = ''; } else Cur += Text[I];
}
if (Cur.trim()) Out.push(Cur.trim());
return Out;
}
/**
* 把字符串/注释内容替换成同长度空格(保留长度,便于用同一套下标扫描括号)。
* 不做这一步的话,字符串里的 `[` `{` 会让括号深度错位,漏掉后面的分支。
*/
function MaskStrings(Text) {
const Out = Text.split('');
let I = 0;
while (I < Text.length) {
const C = Text[I];
if (C === '"' || C === "'" || C === '`') {
const Q = C;
Out[I] = ' ';
I++;
while (I < Text.length) {
if (Text[I] === '\\') { Out[I] = ' '; Out[I + 1] = ' '; I += 2; continue; }
if (Text[I] === Q) { Out[I] = ' '; I++; break; }
if (Text[I] !== '\n') Out[I] = ' ';
I++;
}
continue;
}
if (C === '/' && Text[I + 1] === '/') {
const N = Text.indexOf('\n', I);
const E = N < 0 ? Text.length : N;
for (let J = I; J < E; J++) Out[J] = ' ';
I = E;
continue;
}
if (C === '/' && Text[I + 1] === '*') {
const N = Text.indexOf('*/', I + 2);
const E = N < 0 ? Text.length : N + 2;
for (let J = I; J < E; J++) if (Text[J] !== '\n') Out[J] = ' ';
I = E;
continue;
}
I++;
}
return Out.join('');
}
/** 从 Text[Open] 的 `{` 找到配对的 `}` 下标(掩码判定)。 */
function MatchBrace(Text, Open) {
const M = MaskStrings(Text);
let D = 0;
for (let I = Open; I < Text.length; I++) {
const C = M[I];
if (C === '{') D++;
else if (C === '}') { D--; if (D === 0) return I; }
}
return -1;
}
/** 找顶层 `if (… instanceof …)` 守卫(掩码判深度)。 */
function FindGuards(Text) {
const M = MaskStrings(Text);
const Out = [];
let I = 0, Depth = 0;
while (I < Text.length) {
const C = M[I];
if ('([{'.includes(C)) { Depth++; I++; continue; }
if (')]}'.includes(C)) { Depth--; I++; continue; }
if (Depth === 0 && M.startsWith('if (', I) && M.slice(I, I + 80).includes('instanceof')) {
const OpenP = I + 3;
let D = 0, J = OpenP;
for (; J < Text.length; J++) { const D0 = M[J]; if (D0 === '(') D++; else if (D0 === ')') { D--; if (D === 0) break; } }
Out.push({ At: I, GuardEnd: J, Cond: Text.slice(OpenP + 1, J).replace(/\s+/g, ' ').trim() });
I = J + 1; continue;
}
I++;
}
return Out;
}
/** 从 chunk 里取最浅层的 `i = <expr>`(掩码判深度)。 */
function OuterAssign(Chunk) {
const MC = MaskStrings(Chunk);
let Best = null, BestDepth = Infinity, I2 = 0, D2 = 0;
while (I2 < Chunk.length) {
const C2 = MC[I2];
if ('([{'.includes(C2)) { D2++; I2++; continue; }
if (')]}'.includes(C2)) { D2--; I2++; continue; }
if (MC.startsWith('i =', I2)) {
let J2 = I2 + 3, DD = 0;
while (J2 < Chunk.length) {
const D0 = MC[J2];
if ('([{'.includes(D0)) DD++;
else if (')]}'.includes(D0)) { if (DD === 0) break; DD--; }
else if (D0 === ';' && DD === 0) break;
J2++;
}
if (D2 < BestDepth) { BestDepth = D2; Best = Chunk.slice(I2 + 3, J2).trim(); }
I2 = J2; continue;
}
I2++;
}
return Best;
}
function LocalsIn(Text) {
const Out = [];
for (const M of Text.matchAll(/t instanceof ([A-Za-z0-9_$]+)/g)) Out.push(M[1]);
for (const M of Text.matchAll(/Xd\(t, ([A-Za-z0-9_$]+)\)/g)) Out.push(M[1]);
return [...new Set(Out)];
}
/** 把 `t instanceof A ? e1 : t instanceof B ? e2 : e3` 切成手。 */
function TernaryArms(Text) {
const Hits = [...Text.matchAll(/t instanceof ([A-Za-z0-9_$]+)\s*\?/g)];
return Hits.map((H, Hi) => {
const From = H.index + H[0].length;
const To = Hi + 1 < Hits.length ? Hits[Hi + 1].index : Text.length;
return {
Local: H[1],
Expr: Text.slice(From, To).replace(/^\s*/, '').replace(/[,\s]*:\s*$/, '').replace(/\s*:\s*$/, '').trim(),
};
});
}
/** 槽位 → 字段名 / 字段名 → 索引(按 toString 顺序)。 */
const FieldOfSlot = new Map(RootFields.map((F) => [F.Slot, F.Field]));
function KotlinName(Field) { return Field.charAt(0).toUpperCase() + Field.slice(1); }
// ---------------------------------------------------------------- 单切片计划
function PlanSlice(Field) {
const F = RootFields.find((X) => X.Field === Field);
if (!F) throw new Error('根状态里没有字段 ' + Field);
// 委派有两种形状:
// 1) 根 reducer 内联写 `n.<slot>`(多数切片,或 `n.<slot>.方法(t)`)
// 2) 根 reducer 直接调用具名 reducer `hF(t, n)`,切片槽位只在 hF 体内出现(garage)
// 第 2 种需要回查被调函数的函数体,确认它确实读写 `n.<slot>`。
let A = Assignments.find((X) => new RegExp('\\bn\\.' + F.Slot + '\\b').test(X.Expr));
let NamedReducer = null;
if (!A) {
for (const X of Assignments) {
const Call = /^([A-Za-z0-9_$]+)\(t,\s*n\)$/.exec(X.Expr.replace(/\s+/g, ' ').trim());
if (!Call || !Fns.has(Call[1])) continue;
const Fn = Fns.get(Call[1]);
if (new RegExp('\\bn\\.' + F.Slot + '\\b').test(T.slice(Fn.Body, Fn.End))) {
A = X;
NamedReducer = Call[1];
break;
}
}
}
if (!A) throw new Error(Field + ' 在根 reducer 里找不到委派');
const Param = [...SlotOfParam.keys()].find((P2) => SlotOfParam.get(P2) === F.Slot);
const SliceClass = (DefaultOfParam.get(Param) || '').match(/^new ([A-Za-z0-9_$]+)\(/);
const ClassName = SliceClass ? SliceClass[1] : null;
const Info = {
Field, Slot: F.Slot, Class: ClassName, ClassSource: DefaultOfParam.get(Param) || '',
NamedReducer, Params: [], Fields: [], CopyMethod: null, Branches: [], SubReducers: [], Counts: {}, Locals: {},
Delegate: A.Expr.replace(/\s+/g, ' ').trim().slice(0, 400),
};
if (ClassName) {
const CFn = Fns.get(ClassName);
Info.Params = CFn ? CFn.Params.split(',').map((S) => S.trim()).filter(Boolean) : [];
const CB = CFn ? T.slice(CFn.Body, CFn.End) : '';
const SOfP = new Map();
for (const M of CB.matchAll(/\(this\.([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\)/g)) SOfP.set(M[2], M[1]);
const DOfP = new Map();
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 === cd \? ([\s\S]*?) : \1\)/g)) DOfP.set(M[1], M[2].replace(/\s+/g, ' ').trim());
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 !== cd && \1\)/g)) DOfP.set(M[1], 'false');
for (const M of CB.matchAll(/\(([A-Za-z_$][\w$]*) = \1 === cd \|\| \1\)/g)) DOfP.set(M[1], 'true');
const S = T.indexOf(`(ld(${ClassName}).toString = function`);
const E = T.indexOf(`(ld(${ClassName}).hashCode`, S);
if (S >= 0 && E > S) {
for (const M of T.slice(S, E).matchAll(/"([^"]*?)([A-Za-z0-9_]+)="\s*\+\s*(?:[A-Za-z_$][\w$]*\()?this\.(\w+)/g)) {
const P2 = [...SOfP.keys()].find((K) => SOfP.get(K) === M[3]);
Info.Fields.push({ Field: M[2], Slot: M[3], Kotlin: KotlinName(M[2]), Default: P2 ? DOfP.get(P2) || '' : '' });
}
}
}
const CopyName = (A.Expr.match(/\bn\.([A-Za-z0-9_$]+)\(/) || [])[1] || null;
// 具名 reducer 的副本方法只在函数体里(`o.hri(...)`),按「参数个数 == 字段数」锁定。
let NamedBody = null;
if (NamedReducer) {
const Fn = Fns.get(NamedReducer);
NamedBody = T.slice(Fn.Body, Fn.End);
const ByArity = [];
for (const M of NamedBody.matchAll(/\bo\.([A-Za-z0-9_$]+)\(/g)) {
let O = M.index + M[0].length, D = 0, E = -1;
for (let J = O; J < NamedBody.length; J++) {
const C2 = NamedBody[J];
if (C2 === '(') D++;
else if (C2 === ')') { D--; if (D === 0) { E = J; break; } }
}
if (E < 0) continue;
const N = SplitTop(NamedBody.slice(O, E)).length;
ByArity.push({ Name: M[1], Count: N });
}
const Hit = ByArity.find((X) => X.Count === Info.Fields.length);
Info.CopyMethod = Hit ? Hit.Name : (ByArity[0] || {}).Name || null;
} else {
Info.CopyMethod = CopyName;
}
const CopyMethodName = Info.CopyMethod;
if (CopyMethodName && ClassName) {
const M = Methods.find((X) => X.Obj === ClassName && X.Method === CopyMethodName);
Info.CopyParamCount = M ? M.Params.split(',').map((S) => S.trim()).filter(Boolean).length : null;
Info.CopyMatchesFields = Info.CopyParamCount === Info.Fields.length;
}
// ---- 分支抽取:内联 IIFE 或具名 reducer(`hF(t, n)`)
const Expr = A.Expr;
let Body;
if (NamedReducer) {
Body = NamedBody;
} else if (/^\(function \(t, n\)/.test(Expr)) {
// IIFE 的 { 位置要对到全局源码:用 Pos 定位
const Pos = T.indexOf(A.Expr, RootReducerFn.Body);
const GlobalOpen = T.indexOf('{', T.indexOf('function', Pos));
const GlobalClose = Pairs.get(GlobalOpen);
Body = T.slice(GlobalOpen + 1, GlobalClose);
} else {
Info.Branches = [{ Guard: '(具名 reducer)', Locals: [], Kind: 'named', Expr: Expr.replace(/\s+/g, ' ').slice(0, 200) }];
Info.Counts = { TotalInstanceof: null, TopLevelBranches: 1, SubReducerArms: 0 };
return Info;
}
function Decode(BranchExpr) {
const E = BranchExpr.replace(/\s+/g, ' ').trim();
const Fields = [];
if (/^n$/.test(E)) return { Kind: 'noop', Changed: [] };
const CopyCall = new RegExp('\\b(?:n|o)\\.' + (CopyMethodName || 'qt6') + '\\(');
const M = CopyCall.exec(E);
if (/new [A-Za-z0-9_$]+\(/.test(E) && !M) return { Kind: 'reset', Changed: [] };
if (!M) return { Kind: 'conditional', Changed: [] };
// 找该调用的参数(最外层)
let O = E.indexOf('(', M.index), D = 0, End = -1;
for (let J = O; J < E.length; J++) { const C2 = E[J]; if (C2 === '(') D++; else if (C2 === ')') { D--; if (D === 0) { End = J; break; } } }
const Args = SplitTop(E.slice(O + 1, End));
Args.forEach((Arg, Index) => {
if (Arg === 'cd') return;
const FI = Info.Fields[Index];
Fields.push({ Index, Field: FI ? FI.Field : null, Slot: FI ? FI.Slot : null, Kotlin: FI ? FI.Kotlin : null, Value: Arg });
});
const HasHelper = Fields.some((X) => /[A-Za-z_$][\w$]*\(/.test(X.Value) && !/^t\.[A-Za-z0-9_$]+$/.test(X.Value));
const Simple = Fields.length > 0 && Fields.every((X) => /^t\.[A-Za-z0-9_$]+$/.test(X.Value) || /^(null|!0|!1|true|false|-?\d+)$/.test(X.Value));
return { Kind: Simple || !HasHelper ? 'copy' : 'copy-helper', Changed: Fields };
}
/** 顶层 if/else-if 链 → 分支表。 */
function ChainBranches(Text) {
const Guards = FindGuards(Text);
const Out = [];
Guards.forEach((G, Gi) => {
const NextGuard = Guards[Gi + 1];
const ChunkEnd = NextGuard ? Text.lastIndexOf('else', NextGuard.At) : Text.length;
const Chunk = Text.slice(G.GuardEnd + 1, ChunkEnd);
let BranchExpr = OuterAssign(Chunk);
// 块体分支(例如 KU):`{ var o = t.irl_1; i = null == o ? n : n.qt6(…); }`
// 先剥掉最外层花括号,再取块内最后一个顶层赋值,避免被后续 else 链的三元污染。
const ChunkTrim = Chunk.trim();
if (ChunkTrim.startsWith('{')) {
const Open = Chunk.indexOf('{');
const Close = MatchBrace(Chunk, Open);
if (Close > Open) {
const Inner = OuterAssign(Chunk.slice(Open + 1, Close));
if (Inner !== null) BranchExpr = Inner;
}
}
if (BranchExpr === null) return;
Out.push({ Guard: G.Cond, Locals: LocalsIn(G.Cond), ...Decode(BranchExpr), Expr: BranchExpr.replace(/\s+/g, ' ').trim().slice(0, 300), _Chunk: Chunk });
});
// 末尾 else:`else i = t instanceof m5 ? … : t instanceof f5 ? … : n;`
if (Guards.length) {
const LastEnd = Guards[Guards.length - 1].GuardEnd;
const Tail = Text.slice(LastEnd);
const ElseAt = Tail.lastIndexOf('else');
if (ElseAt >= 0) {
const BranchExpr = OuterAssign(Tail.slice(ElseAt));
if (BranchExpr) {
const Arms = TernaryArms(BranchExpr);
for (const Arm of Arms) {
Out.push({ Guard: 't instanceof ' + Arm.Local, Locals: [Arm.Local], ...Decode(Arm.Expr), Expr: Arm.Expr.replace(/\s+/g, ' ').trim().slice(0, 300) });
}
if (!Arms.length) Out.push({ Guard: '(else)', Locals: [], ...Decode(BranchExpr), Expr: BranchExpr.replace(/\s+/g, ' ').trim().slice(0, 300) });
}
}
}
return Out;
}
/** 分支里内嵌的 `(function (t, n) { … })(t, <arg>)` 子 reducer。 */
function NestedReducers(Chunk) {
const Out = [];
const Re = /(?:var\s+([A-Za-z0-9_$]+)\s*=\s*)?\(function \(t, n\)\s*\{/g;
let M;
while ((M = Re.exec(Chunk))) {
const Open = Chunk.indexOf('{', M.index + M[0].length - 1);
const Close = MatchBrace(Chunk, Open);
if (Close < 0) continue;
const Body2 = Chunk.slice(Open + 1, Close);
const After = Chunk.slice(Close + 1, Close + 120);
const CallArg = (After.match(/^\)\(t,\s*([^)]+)\)/) || [])[1] || null;
const IsTernary = /\breturn\s+t instanceof [A-Za-z0-9_$]+\s*\?/.test(Body2);
let Arms;
if (IsTernary) {
const Ret = Body2.slice(Body2.search(/\breturn\s+t instanceof /));
Arms = TernaryArms(Ret).map((Arm) => ({ Local: Arm.Local, Expr: Arm.Expr.replace(/\s+/g, ' ').slice(0, 200) }));
} else {
Arms = ChainBranches(Body2).map((B) => ({
Local: (B.Locals || [])[0] || null,
Guard: B.Guard,
Kind: B.Kind,
Expr: B.Expr,
}));
}
Out.push({
Var: M[1] || null,
Arg: CallArg ? CallArg.trim() : null,
Style: IsTernary ? 'ternary' : 'chain',
ArmCount: Arms.length,
Locals: Arms.map((X) => X.Local).filter(Boolean).map((L) => ({ Local: L, Name: LocalToName.get(L) || null })),
Arms,
});
Re.lastIndex = Close + 1;
}
return Out;
}
const Top = ChainBranches(Body);
Info.Branches = Top.map(({ _Chunk, ...Rest }) => Rest);
for (const B of Top) {
if (B._Chunk) Info.SubReducers.push(...NestedReducers(B._Chunk));
}
Info.Counts = {
TotalInstanceof: (Body.match(/\bt instanceof [A-Za-z0-9_$]+/g) || []).length,
TopLevelBranches: Info.Branches.length,
SubReducerArms: Info.SubReducers.reduce((N, S) => N + S.ArmCount, 0),
};
// 局部类命名情况
const Collect = (L) => { if (!Info.Locals[L]) Info.Locals[L] = { Name: LocalToName.get(L) || null }; };
for (const B of Info.Branches) for (const L of B.Locals || []) Collect(L);
for (const S of Info.SubReducers) for (const X of S.Locals) Collect(X.Local);
return Info;
}
// ---------------------------------------------------------------- 输出
const Want = process.argv.slice(2);
if (!Want.length) throw new Error('用法:node gen_slice_plan.js <field> [field ...]');
for (const Field of Want) {
const Info = PlanSlice(Field);
const OutFile = path.join(P, 'names', 'slice_' + Field + '.json');
fs.writeFileSync(OutFile, JSON.stringify({ generatedAt: new Date().toISOString(), module: 'module_10032.js', ...Info }, null, 2) + '\n');
const Kinds = {};
for (const B of Info.Branches) Kinds[B.Kind] = (Kinds[B.Kind] || 0) + 1;
const Anon = Object.entries(Info.Locals).filter(([, V]) => !V.Name).map(([K]) => K);
const C = Info.Counts || {};
console.log(`${Field}: class=${Info.Class} 字段=${Info.Fields.length} copy=${Info.CopyMethod}(${Info.CopyParamCount}) 顶层分支=${Info.Branches.length} ${JSON.stringify(Kinds)}`);
console.log(` 实例判定=${C.TotalInstanceof} 子reducer=${(Info.SubReducers || []).length} 子reducer分支=${C.SubReducerArms}`);
for (const S of Info.SubReducers || []) console.log(` 子reducer var=${S.Var} arg=${S.Arg} style=${S.Style} arms=${S.ArmCount}`);
if (Anon.length) console.log(' 未具名局部类: ' + Anon.join(', '));
}