luau-obfuscator 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -594,7 +594,7 @@ function splitByteChunks(bytes, min, max) {
594
594
  return chunks.length > 0 ? chunks : [bytes];
595
595
  }
596
596
  function stringCharCall(bytes) {
597
- return call(member(identifier("string"), "char"), bytes.map(numberLiteral));
597
+ return call(member(identifier("string"), "char"), bytes.map(vmNumberLiteral));
598
598
  }
599
599
  function buildConcatChain(chunks) {
600
600
  function build(lo, hi) {
@@ -625,7 +625,7 @@ function buildShuffledTables(chunks) {
625
625
  for (let slot = 0; slot < n; slot++) {
626
626
  order[storageOrder[slot]] = slot + 1;
627
627
  }
628
- const orderFields = order.map((slot) => positionalField(numberLiteral(slot)));
628
+ const orderFields = order.map((slot) => positionalField(vmNumberLiteral(slot)));
629
629
  return {
630
630
  storeVar,
631
631
  orderVar,
@@ -644,7 +644,7 @@ function buildForLoopStringExpr(chunks) {
644
644
  localStatement(accVar, stringLiteral("")),
645
645
  numericForStatement(
646
646
  iVar,
647
- numberLiteral(1),
647
+ vmNumberLiteral(1),
648
648
  unary("#", identifier(orderVar)),
649
649
  block([
650
650
  assignmentStatement(
@@ -676,7 +676,7 @@ function buildRecursiveStringExpr(chunks) {
676
676
  ]),
677
677
  returnStatement([
678
678
  call(identifier(recName), [
679
- binary("+", identifier(iParam), numberLiteral(1)),
679
+ binary("+", identifier(iParam), vmNumberLiteral(1)),
680
680
  binary(
681
681
  "..",
682
682
  identifier(accParam),
@@ -688,7 +688,7 @@ function buildRecursiveStringExpr(chunks) {
688
688
  return iife([
689
689
  ...stmts,
690
690
  localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
691
- returnStatement([call(identifier(recName), [numberLiteral(1), stringLiteral("")])])
691
+ returnStatement([call(identifier(recName), [vmNumberLiteral(1), stringLiteral("")])])
692
692
  ]);
693
693
  }
694
694
  function pickControlFlowStrategy() {
@@ -1766,9 +1766,10 @@ var DEFAULT_BUILTIN_GLOBALS = [
1766
1766
  "next"
1767
1767
  ];
1768
1768
  var VmCompiler = class {
1769
- constructor(program, builtinGlobals = DEFAULT_BUILTIN_GLOBALS, opcodeMap) {
1769
+ constructor(program, builtinGlobals = DEFAULT_BUILTIN_GLOBALS, opcodeMap, debug = false) {
1770
1770
  this.program = program;
1771
1771
  this.opcodeMap = opcodeMap;
1772
+ this.debug = debug;
1772
1773
  this.analysis = import_luau_parser3.luauparser.analyzeScopes(program, { builtinGlobals });
1773
1774
  this.enclosing = buildEnclosingFunctionMap(program);
1774
1775
  for (const [id, binding] of this.analysis.bindings) {
@@ -1778,11 +1779,29 @@ var VmCompiler = class {
1778
1779
  }
1779
1780
  program;
1780
1781
  opcodeMap;
1782
+ debug;
1781
1783
  analysis;
1782
1784
  enclosing;
1783
1785
  captured = /* @__PURE__ */ new Set();
1784
1786
  protoIdCounter = 0;
1785
1787
  declToBinding = /* @__PURE__ */ new Map();
1788
+ /**
1789
+ * 실제로 프로그램이 참조하는 모든 전역 이름(사전 등록된 builtinGlobals +
1790
+ * 스코프 분석이 자동으로 찾아낸, 로컬/파라미터/업밸류로 안 풀리는 나머지 식별자
1791
+ * 전부)을 돌려준다. Vmify.ts가 이 목록으로 브릿지 테이블(globals)을 만든다.
1792
+ *
1793
+ * 예전엔 호출자가 넘긴 builtinGlobals 목록만 브릿지에 넣었는데, 그 목록에
1794
+ * 없는 전역(예: Roblox API나 실행기 전용 전역을 다 못 채운 경우)을 참조하면
1795
+ * 컴파일은 성공하지만 GETGLOBAL이 조용히 nil을 반환해서 "attempt to call a
1796
+ * nil value"로 터진다 — 게다가 원인이 VM 디스패치 쪽 스택에서만 보여서
1797
+ * "opcode 매핑이 깨졌나?" 하고 엉뚱한 곳을 의심하게 만든다. analyzeScopes는
1798
+ * 어차피 로컬/업밸류로 못 푸는 식별자를 전부 global 바인딩으로 잡아두므로,
1799
+ * builtinGlobals에 없던 이름도 여기 globalsByName에는 다 들어있다 —
1800
+ * 하드코딩된 허용목록 대신 이걸 그대로 쓰면 커버리지 문제 자체가 사라진다.
1801
+ */
1802
+ getUsedGlobalNames() {
1803
+ return Array.from(this.analysis.globalsByName.keys());
1804
+ }
1786
1805
  computeCaptured() {
1787
1806
  for (const binding of this.analysis.bindings.values()) {
1788
1807
  if (binding.kind === "global") continue;
@@ -1819,9 +1838,39 @@ var VmCompiler = class {
1819
1838
  }
1820
1839
  emit(state, op, a, b, c, comment) {
1821
1840
  const physicalOp = this.opcodeMap ? this.opcodeMap[op] : op;
1822
- state.proto.code.push({ op: physicalOp, a, b, c, comment });
1841
+ state.proto.code.push({ op: physicalOp, a, b, c, comment, dbgLine: this.debug ? state.curLine : void 0 });
1823
1842
  return state.proto.code.length - 1;
1824
1843
  }
1844
+ /** debug 빌드: 방금 emit한 CALL 인스트럭션에 "호출 대상 표현식 문자열"과 그 소스 줄을
1845
+ * 박아넣는다(런타임이 non-callable을 잡았을 때 사람이 읽을 위치를 찍게). */
1846
+ stampCall(state, idx, callExpr, calleeExpr) {
1847
+ if (!this.debug) return;
1848
+ const instr = state.proto.code[idx];
1849
+ instr.dbgName = this.describeExpr(calleeExpr);
1850
+ const line = calleeExpr.line ?? callExpr.line;
1851
+ if (line) instr.dbgLine = line.start;
1852
+ }
1853
+ /** 표현식을 사람이 읽을 짧은 문자열로. 위치 추적용이라 완벽할 필요는 없음. */
1854
+ describeExpr(e) {
1855
+ switch (e.type) {
1856
+ case "Identifier":
1857
+ return e.name;
1858
+ case "MemberExpression":
1859
+ return `${this.describeExpr(e.object)}.${e.property.name}`;
1860
+ case "MethodCallExpression":
1861
+ return `${this.describeExpr(e.object)}:${e.method.name}(...)`;
1862
+ case "IndexExpression":
1863
+ return `${this.describeExpr(e.object)}[...]`;
1864
+ case "CallExpression":
1865
+ return `${this.describeExpr(e.callee)}(...)`;
1866
+ case "ParenthesizedExpression":
1867
+ return `(${this.describeExpr(e.expression)})`;
1868
+ case "StringLiteral":
1869
+ return `"${String(e.value).slice(0, 20)}"`;
1870
+ default:
1871
+ return `<${e.type}>`;
1872
+ }
1873
+ }
1825
1874
  konst(state, value) {
1826
1875
  return internConst(state.proto, value);
1827
1876
  }
@@ -1913,6 +1962,7 @@ var VmCompiler = class {
1913
1962
  state.regs.releaseLocalsTo(saved);
1914
1963
  }
1915
1964
  compileStatement(stmt, state) {
1965
+ if (this.debug && stmt.line) state.curLine = stmt.line.start;
1916
1966
  switch (stmt.type) {
1917
1967
  case "LocalStatement": {
1918
1968
  const bindingIds = stmt.names.map((n) => this.bindingIdOfDecl(n));
@@ -1938,11 +1988,26 @@ var VmCompiler = class {
1938
1988
  return;
1939
1989
  }
1940
1990
  case "AssignmentStatement": {
1941
- const valueRegs = stmt.values.map((e) => this.compileExpr(e, state));
1942
- stmt.targets.forEach((target, i) => {
1943
- const srcReg = valueRegs[i] ?? valueRegs[valueRegs.length - 1];
1944
- this.compileAssignTarget(target, srcReg, state);
1945
- });
1991
+ const targets = stmt.targets;
1992
+ const values = stmt.values;
1993
+ const base = state.regs.top();
1994
+ if (values.length > 0) {
1995
+ for (let i = 0; i < values.length - 1; i++) {
1996
+ this.compileExprTo(values[i], state, base + i);
1997
+ }
1998
+ const lastIdx = values.length - 1;
1999
+ const remaining = Math.max(targets.length - lastIdx, 1);
2000
+ this.compileExprMultiInto(values[lastIdx], state, base + lastIdx, remaining);
2001
+ }
2002
+ const produced = values.length === 0 ? 0 : values.length - 1 + Math.max(targets.length - (values.length - 1), 1);
2003
+ for (let i = produced; i < targets.length; i++) {
2004
+ this.emit(state, 3 /* LOADNIL */, base + i, base + i, 0);
2005
+ }
2006
+ for (let i = 0; i < targets.length; i++) {
2007
+ state.regs.freeTemp(base + targets.length);
2008
+ this.compileAssignTarget(targets[i], base + i, state);
2009
+ }
2010
+ state.regs.freeTemp(base);
1946
2011
  return;
1947
2012
  }
1948
2013
  case "CompoundAssignmentStatement":
@@ -2286,7 +2351,7 @@ var VmCompiler = class {
2286
2351
  compileCallMultiInto(callExpr, state, wantCount) {
2287
2352
  const base = this.compileExpr(callExpr.callee, state);
2288
2353
  this.compileArgsContiguous(callExpr.arguments, state, base + 1);
2289
- this.emit(state, 27 /* CALL */, base, callExpr.arguments.length + 1, wantCount + 1);
2354
+ this.stampCall(state, this.emit(state, 27 /* CALL */, base, callExpr.arguments.length + 1, wantCount + 1), callExpr, callExpr.callee);
2290
2355
  return base;
2291
2356
  }
2292
2357
  loadConst(state, value) {
@@ -2337,7 +2402,7 @@ var VmCompiler = class {
2337
2402
  if (expr.type === "CallExpression") {
2338
2403
  const calleeReg = this.compileExpr(expr.callee, state);
2339
2404
  const b = this.compileCallArgsAndGetB(expr.arguments, state, calleeReg + 1);
2340
- this.emit(state, 27 /* CALL */, calleeReg, b, wantCount + 1);
2405
+ this.stampCall(state, this.emit(state, 27 /* CALL */, calleeReg, b, wantCount + 1), expr, expr.callee);
2341
2406
  for (let i = 0; i < wantCount; i++) {
2342
2407
  if (calleeReg + i !== base + i) this.emit(state, 0 /* MOVE */, base + i, calleeReg + i, 0);
2343
2408
  }
@@ -2352,7 +2417,7 @@ var VmCompiler = class {
2352
2417
  state.regs.allocTemp();
2353
2418
  const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2);
2354
2419
  const b = argB === 0 ? 0 : expr.arguments.length + 2;
2355
- this.emit(state, 27 /* CALL */, fnSlot, b, wantCount + 1);
2420
+ this.stampCall(state, this.emit(state, 27 /* CALL */, fnSlot, b, wantCount + 1), expr, expr);
2356
2421
  for (let i = 0; i < wantCount; i++) {
2357
2422
  if (fnSlot + i !== base + i) this.emit(state, 0 /* MOVE */, base + i, fnSlot + i, 0);
2358
2423
  }
@@ -2378,7 +2443,7 @@ var VmCompiler = class {
2378
2443
  if (calleeReg !== base) this.emit(state, 0 /* MOVE */, base, calleeReg, 0);
2379
2444
  state.regs.freeTemp(base + 1);
2380
2445
  const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1);
2381
- this.emit(state, 27 /* CALL */, base, b, 0);
2446
+ this.stampCall(state, this.emit(state, 27 /* CALL */, base, b, 0), expr, expr.callee);
2382
2447
  return;
2383
2448
  }
2384
2449
  if (expr.type === "MethodCallExpression") {
@@ -2394,7 +2459,7 @@ var VmCompiler = class {
2394
2459
  state.regs.freeTemp(base + 2);
2395
2460
  const argB = this.compileCallArgsAndGetB(expr.arguments, state, base + 2);
2396
2461
  const b = argB === 0 ? 0 : expr.arguments.length + 2;
2397
- this.emit(state, 27 /* CALL */, base, b, 0);
2462
+ this.stampCall(state, this.emit(state, 27 /* CALL */, base, b, 0), expr, expr);
2398
2463
  return;
2399
2464
  }
2400
2465
  if (expr.type === "VarargExpression") {
@@ -2421,6 +2486,7 @@ var VmCompiler = class {
2421
2486
  return args.length + 1;
2422
2487
  }
2423
2488
  compileExpr(expr, state, discard = false) {
2489
+ if (this.debug && expr.line) state.curLine = expr.line.start;
2424
2490
  switch (expr.type) {
2425
2491
  case "NilLiteral": {
2426
2492
  const r = state.regs.allocTemp();
@@ -2578,7 +2644,8 @@ var VmCompiler = class {
2578
2644
  case "CallExpression": {
2579
2645
  const base = this.compileExpr(expr.callee, state);
2580
2646
  const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1);
2581
- this.emit(state, 27 /* CALL */, base, b, discard ? 1 : 2);
2647
+ const callIdx = this.emit(state, 27 /* CALL */, base, b, discard ? 1 : 2);
2648
+ this.stampCall(state, callIdx, expr, expr.callee);
2582
2649
  state.regs.freeTemp(base + (discard ? 0 : 1));
2583
2650
  return base;
2584
2651
  }
@@ -2590,7 +2657,8 @@ var VmCompiler = class {
2590
2657
  state.regs.allocTemp();
2591
2658
  const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2);
2592
2659
  const b = argB === 0 ? 0 : expr.arguments.length + 2;
2593
- this.emit(state, 27 /* CALL */, fnSlot, b, discard ? 1 : 2);
2660
+ const callIdx = this.emit(state, 27 /* CALL */, fnSlot, b, discard ? 1 : 2);
2661
+ this.stampCall(state, callIdx, expr, expr);
2594
2662
  state.regs.freeTemp(fnSlot + (discard ? 0 : 1));
2595
2663
  return fnSlot;
2596
2664
  }
@@ -2703,15 +2771,20 @@ function serializeConstValue(v) {
2703
2771
  if (typeof v === "number") return numberLiteral(v);
2704
2772
  return booleanLiteral(v);
2705
2773
  }
2706
- function serializeInstrs(proto) {
2707
- const fields = proto.code.map((instr) => positionalField(
2708
- table([
2774
+ function serializeInstrs(proto, debug) {
2775
+ const fields = proto.code.map((instr) => {
2776
+ const tuple = [
2709
2777
  positionalField(vmNumberLiteral(instr.op)),
2710
2778
  positionalField(vmNumberLiteral(instr.a)),
2711
2779
  positionalField(vmNumberLiteral(instr.b)),
2712
2780
  positionalField(vmNumberLiteral(instr.c))
2713
- ])
2714
- ));
2781
+ ];
2782
+ if (debug) {
2783
+ tuple.push(positionalField(numberLiteral(instr.dbgLine ?? 0)));
2784
+ tuple.push(positionalField(stringLiteral(instr.dbgName ?? "")));
2785
+ }
2786
+ return positionalField(table(tuple));
2787
+ });
2715
2788
  return table(fields);
2716
2789
  }
2717
2790
  function serializeUpvalDescs(proto, names) {
@@ -2723,29 +2796,41 @@ function serializeUpvalDescs(proto, names) {
2723
2796
  ));
2724
2797
  return table(fields);
2725
2798
  }
2726
- function serializeProto(proto, names) {
2799
+ function serializeProto(proto, names, debug = false) {
2727
2800
  const fields = [
2728
2801
  namedField(names.numParams, vmNumberLiteral(proto.numParams)),
2729
2802
  namedField(names.hasVarargs, booleanLiteral(proto.hasVarargs)),
2730
2803
  namedField(names.maxRegs, vmNumberLiteral(proto.maxRegs)),
2731
- namedField(names.code, serializeInstrs(proto)),
2804
+ namedField(names.code, serializeInstrs(proto, debug)),
2732
2805
  namedField(names.consts, serializeConsts(proto.consts)),
2733
2806
  namedField(names.upvalDescs, serializeUpvalDescs(proto, names)),
2734
- namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names)))))
2807
+ namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names, debug)))))
2735
2808
  ];
2736
2809
  return table(fields);
2737
2810
  }
2738
2811
 
2739
2812
  // src/passes/vmify/runtime.ts
2740
- function buildVmRuntimeSource(names, opcodeMap) {
2813
+ function buildVmRuntimeSource(names, opcodeMap, debug = false) {
2741
2814
  const N = names;
2742
2815
  const op = (o) => opcodeMap[o];
2743
2816
  return `
2744
2817
  local ${N.handlers} = {}
2745
2818
 
2819
+ -- \uAC01 \uD578\uB4E4\uB7EC\uB294 "\uB2E4\uC74C pc(\uC22B\uC790)"\uB97C \uBC18\uD658\uD55C\uB2E4. RETURN \uD578\uB4E4\uB7EC\uB9CC table.pack \uACB0\uACFC(\uD14C\uC774\uBE14)\uB97C
2820
+ -- \uBC18\uD658\uD558\uBA70, \uADF8\uAC8C \uD568\uC218 \uC2E4\uD589 \uC885\uB8CC \uC2E0\uD638\uB2E4. Luau\uB294 proper tail call\uC744 \uD558\uC9C0 \uC54A\uC73C\uBBC0\uB85C
2821
+ -- \uD578\uB4E4\uB7EC\uB4E4\uC774 \uC11C\uB85C \uAF2C\uB9AC \uD638\uCD9C\uB85C \uC774\uC5B4\uC9C0\uBA74 \uBA85\uB839\uC5B4\uB2F9 \uC2A4\uD0DD \uD504\uB808\uC784\uC774 \uC313\uC5EC \uD130\uC9C4\uB2E4 \u2014 \uADF8\uB798\uC11C
2822
+ -- \uC5EC\uAE30\uC11C while \uB8E8\uD504\uB85C \uB3CC\uB824 \uC2A4\uD0DD \uAE4A\uC774\uB97C \uD568\uC218 \uC2E4\uD589 \uB0B4\uB0B4 \uC77C\uC815\uD558\uAC8C \uC720\uC9C0\uD55C\uB2E4.
2746
2823
  local function ${N.dispatch}(${N.frame}, pc)
2747
- local instr = ${N.frame}.${N.code}[pc]
2748
- return ${N.handlers}[instr[1]](${N.frame}, pc, instr)
2824
+ local ${N.handlers} = ${N.handlers}
2825
+ while true do
2826
+ local instr = ${N.frame}.${N.code}[pc]
2827
+ local next = ${N.handlers}[instr[1]](${N.frame}, pc, instr)
2828
+ if type(next) == "number" then
2829
+ pc = next
2830
+ else
2831
+ return table.unpack(next, 1, next.n)
2832
+ end
2833
+ end
2749
2834
  end
2750
2835
 
2751
2836
  local function ${N.rk}(${N.frame}, x)
@@ -2759,67 +2844,67 @@ end
2759
2844
  -- MOVE
2760
2845
  ${N.handlers}[${op(0 /* MOVE */)}] = function(${N.frame}, pc, instr)
2761
2846
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.R}[instr[3]]
2762
- return ${N.dispatch}(${N.frame}, pc + 1)
2847
+ return pc + 1
2763
2848
  end
2764
2849
 
2765
2850
  -- LOADK
2766
2851
  ${N.handlers}[${op(1 /* LOADK */)}] = function(${N.frame}, pc, instr)
2767
2852
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.K}[instr[3] + 1]
2768
- return ${N.dispatch}(${N.frame}, pc + 1)
2853
+ return pc + 1
2769
2854
  end
2770
2855
 
2771
2856
  -- LOADBOOL
2772
2857
  ${N.handlers}[${op(2 /* LOADBOOL */)}] = function(${N.frame}, pc, instr)
2773
2858
  ${N.frame}.${N.R}[instr[2]] = (instr[3] ~= 0)
2774
- return ${N.dispatch}(${N.frame}, pc + 1)
2859
+ return pc + 1
2775
2860
  end
2776
2861
 
2777
2862
  -- LOADNIL
2778
2863
  ${N.handlers}[${op(3 /* LOADNIL */)}] = function(${N.frame}, pc, instr)
2779
2864
  for i = instr[2], instr[3] do ${N.frame}.${N.R}[i] = nil end
2780
- return ${N.dispatch}(${N.frame}, pc + 1)
2865
+ return pc + 1
2781
2866
  end
2782
2867
 
2783
2868
  -- GETUPVAL
2784
2869
  ${N.handlers}[${op(4 /* GETUPVAL */)}] = function(${N.frame}, pc, instr)
2785
2870
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.upvals}[instr[3] + 1]
2786
- return ${N.dispatch}(${N.frame}, pc + 1)
2871
+ return pc + 1
2787
2872
  end
2788
2873
 
2789
2874
  -- SETUPVAL
2790
2875
  ${N.handlers}[${op(5 /* SETUPVAL */)}] = function(${N.frame}, pc, instr)
2791
2876
  ${N.frame}.${N.upvals}[instr[3] + 1] = ${N.frame}.${N.R}[instr[2]]
2792
- return ${N.dispatch}(${N.frame}, pc + 1)
2877
+ return pc + 1
2793
2878
  end
2794
2879
 
2795
2880
  -- GETGLOBAL (b\uB294 RK\uAC00 \uC544\uB2C8\uB77C \uC0C1\uC218\uD480 \uC21C\uC218 \uC778\uB371\uC2A4)
2796
2881
  ${N.handlers}[${op(6 /* GETGLOBAL */)}] = function(${N.frame}, pc, instr)
2797
2882
  ${N.frame}.${N.R}[instr[2]] = ${N.globals}[${N.frame}.${N.K}[instr[3] + 1]]
2798
- return ${N.dispatch}(${N.frame}, pc + 1)
2883
+ return pc + 1
2799
2884
  end
2800
2885
 
2801
2886
  -- SETGLOBAL
2802
2887
  ${N.handlers}[${op(7 /* SETGLOBAL */)}] = function(${N.frame}, pc, instr)
2803
2888
  ${N.globals}[${N.frame}.${N.K}[instr[3] + 1]] = ${N.frame}.${N.R}[instr[2]]
2804
- return ${N.dispatch}(${N.frame}, pc + 1)
2889
+ return pc + 1
2805
2890
  end
2806
2891
 
2807
2892
  -- GETTABLE
2808
2893
  ${N.handlers}[${op(8 /* GETTABLE */)}] = function(${N.frame}, pc, instr)
2809
2894
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.R}[instr[3]][${N.rk}(${N.frame}, instr[4])]
2810
- return ${N.dispatch}(${N.frame}, pc + 1)
2895
+ return pc + 1
2811
2896
  end
2812
2897
 
2813
2898
  -- SETTABLE
2814
2899
  ${N.handlers}[${op(9 /* SETTABLE */)}] = function(${N.frame}, pc, instr)
2815
2900
  ${N.frame}.${N.R}[instr[2]][${N.rk}(${N.frame}, instr[3])] = ${N.rk}(${N.frame}, instr[4])
2816
- return ${N.dispatch}(${N.frame}, pc + 1)
2901
+ return pc + 1
2817
2902
  end
2818
2903
 
2819
2904
  -- NEWTABLE
2820
2905
  ${N.handlers}[${op(10 /* NEWTABLE */)}] = function(${N.frame}, pc, instr)
2821
2906
  ${N.frame}.${N.R}[instr[2]] = {}
2822
- return ${N.dispatch}(${N.frame}, pc + 1)
2907
+ return pc + 1
2823
2908
  end
2824
2909
 
2825
2910
  -- SELF (R[a] = R[b][key]; R[a+1] = R[b])
@@ -2827,90 +2912,90 @@ ${N.handlers}[${op(11 /* SELF */)}] = function(${N.frame}, pc, instr)
2827
2912
  local obj = ${N.frame}.${N.R}[instr[3]]
2828
2913
  ${N.frame}.${N.R}[instr[2]] = obj[${N.rk}(${N.frame}, instr[4])]
2829
2914
  ${N.frame}.${N.R}[instr[2] + 1] = obj
2830
- return ${N.dispatch}(${N.frame}, pc + 1)
2915
+ return pc + 1
2831
2916
  end
2832
2917
 
2833
2918
  -- ADD SUB MUL DIV MOD POW
2834
2919
  ${N.handlers}[${op(12 /* ADD */)}] = function(${N.frame}, pc, instr)
2835
2920
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) + ${N.rk}(${N.frame}, instr[4])
2836
- return ${N.dispatch}(${N.frame}, pc + 1)
2921
+ return pc + 1
2837
2922
  end
2838
2923
  ${N.handlers}[${op(13 /* SUB */)}] = function(${N.frame}, pc, instr)
2839
2924
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) - ${N.rk}(${N.frame}, instr[4])
2840
- return ${N.dispatch}(${N.frame}, pc + 1)
2925
+ return pc + 1
2841
2926
  end
2842
2927
  ${N.handlers}[${op(14 /* MUL */)}] = function(${N.frame}, pc, instr)
2843
2928
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) * ${N.rk}(${N.frame}, instr[4])
2844
- return ${N.dispatch}(${N.frame}, pc + 1)
2929
+ return pc + 1
2845
2930
  end
2846
2931
  ${N.handlers}[${op(15 /* DIV */)}] = function(${N.frame}, pc, instr)
2847
2932
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) / ${N.rk}(${N.frame}, instr[4])
2848
- return ${N.dispatch}(${N.frame}, pc + 1)
2933
+ return pc + 1
2849
2934
  end
2850
2935
  ${N.handlers}[${op(16 /* MOD */)}] = function(${N.frame}, pc, instr)
2851
2936
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) % ${N.rk}(${N.frame}, instr[4])
2852
- return ${N.dispatch}(${N.frame}, pc + 1)
2937
+ return pc + 1
2853
2938
  end
2854
2939
  ${N.handlers}[${op(17 /* POW */)}] = function(${N.frame}, pc, instr)
2855
2940
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) ^ ${N.rk}(${N.frame}, instr[4])
2856
- return ${N.dispatch}(${N.frame}, pc + 1)
2941
+ return pc + 1
2857
2942
  end
2858
2943
 
2859
2944
  -- IDIV (Luau \uBC14\uB2E5 \uB098\uB217\uC148)
2860
2945
  ${N.handlers}[${op(34 /* IDIV */)}] = function(${N.frame}, pc, instr)
2861
2946
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) // ${N.rk}(${N.frame}, instr[4])
2862
- return ${N.dispatch}(${N.frame}, pc + 1)
2947
+ return pc + 1
2863
2948
  end
2864
2949
 
2865
2950
  -- CONCAT
2866
2951
  ${N.handlers}[${op(18 /* CONCAT */)}] = function(${N.frame}, pc, instr)
2867
2952
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) .. ${N.rk}(${N.frame}, instr[4])
2868
- return ${N.dispatch}(${N.frame}, pc + 1)
2953
+ return pc + 1
2869
2954
  end
2870
2955
 
2871
2956
  -- UNM
2872
2957
  ${N.handlers}[${op(19 /* UNM */)}] = function(${N.frame}, pc, instr)
2873
2958
  ${N.frame}.${N.R}[instr[2]] = -${N.frame}.${N.R}[instr[3]]
2874
- return ${N.dispatch}(${N.frame}, pc + 1)
2959
+ return pc + 1
2875
2960
  end
2876
2961
 
2877
2962
  -- NOT
2878
2963
  ${N.handlers}[${op(20 /* NOT */)}] = function(${N.frame}, pc, instr)
2879
2964
  ${N.frame}.${N.R}[instr[2]] = not ${N.frame}.${N.R}[instr[3]]
2880
- return ${N.dispatch}(${N.frame}, pc + 1)
2965
+ return pc + 1
2881
2966
  end
2882
2967
 
2883
2968
  -- LEN
2884
2969
  ${N.handlers}[${op(21 /* LEN */)}] = function(${N.frame}, pc, instr)
2885
2970
  ${N.frame}.${N.R}[instr[2]] = #${N.frame}.${N.R}[instr[3]]
2886
- return ${N.dispatch}(${N.frame}, pc + 1)
2971
+ return pc + 1
2887
2972
  end
2888
2973
 
2889
2974
  -- JMP (pc += a)
2890
2975
  ${N.handlers}[${op(22 /* JMP */)}] = function(${N.frame}, pc, instr)
2891
- return ${N.dispatch}(${N.frame}, pc + instr[2])
2976
+ return pc + instr[2]
2892
2977
  end
2893
2978
 
2894
2979
  -- EQ LT LE (\uAC12 \uC0DD\uC131\uD615 \uBE44\uAD50)
2895
2980
  ${N.handlers}[${op(23 /* EQ */)}] = function(${N.frame}, pc, instr)
2896
2981
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) == ${N.rk}(${N.frame}, instr[4])
2897
- return ${N.dispatch}(${N.frame}, pc + 1)
2982
+ return pc + 1
2898
2983
  end
2899
2984
  ${N.handlers}[${op(24 /* LT */)}] = function(${N.frame}, pc, instr)
2900
2985
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) < ${N.rk}(${N.frame}, instr[4])
2901
- return ${N.dispatch}(${N.frame}, pc + 1)
2986
+ return pc + 1
2902
2987
  end
2903
2988
  ${N.handlers}[${op(25 /* LE */)}] = function(${N.frame}, pc, instr)
2904
2989
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) <= ${N.rk}(${N.frame}, instr[4])
2905
- return ${N.dispatch}(${N.frame}, pc + 1)
2990
+ return pc + 1
2906
2991
  end
2907
2992
 
2908
2993
  -- TEST (if (not R[a]) != (c!=0) then pc += 2 else pc += 1 \u2014 \uBCF4\uD1B5 \uB2E4\uC74C \uBA85\uB839\uC740 JMP)
2909
2994
  ${N.handlers}[${op(26 /* TEST */)}] = function(${N.frame}, pc, instr)
2910
2995
  if (not ${N.frame}.${N.R}[instr[2]]) ~= (instr[4] ~= 0) then
2911
- return ${N.dispatch}(${N.frame}, pc + 2)
2996
+ return pc + 2
2912
2997
  end
2913
- return ${N.dispatch}(${N.frame}, pc + 1)
2998
+ return pc + 1
2914
2999
  end
2915
3000
 
2916
3001
  -- CALL
@@ -2925,7 +3010,13 @@ ${N.handlers}[${op(27 /* CALL */)}] = function(${N.frame}, pc, instr)
2925
3010
  end
2926
3011
  local args = table.create(nargs)
2927
3012
  for i = 1, nargs do args[i] = ${N.frame}.${N.R}[a + i] end
2928
- local rets = table.pack(fn(table.unpack(args, 1, nargs)))
3013
+ ${debug ? ` if type(fn) ~= "function" then
3014
+ local mt = getmetatable(fn)
3015
+ if not (mt and mt.__call) then
3016
+ error(("[vmdebug] attempt to call a %s value: '%s' (source line %d)"):format(type(fn), tostring(instr[6]), tonumber(instr[5]) or -1), 0)
3017
+ end
3018
+ end
3019
+ ` : ""} local rets = table.pack(fn(table.unpack(args, 1, nargs)))
2929
3020
  if c == 1 then
2930
3021
  -- discard
2931
3022
  elseif c == 0 then
@@ -2935,23 +3026,27 @@ ${N.handlers}[${op(27 /* CALL */)}] = function(${N.frame}, pc, instr)
2935
3026
  local want = c - 1
2936
3027
  for i = 1, want do ${N.frame}.${N.R}[a + i - 1] = rets[i] end
2937
3028
  end
2938
- return ${N.dispatch}(${N.frame}, pc + 1)
3029
+ return pc + 1
2939
3030
  end
2940
3031
 
2941
3032
  -- RETURN
3033
+ -- \uB2E4\uB978 \uD578\uB4E4\uB7EC\uB294 "\uB2E4\uC74C pc(\uC22B\uC790)"\uB97C \uBC18\uD658\uD558\uC9C0\uB9CC, RETURN\uB9CC\uC740 \uACB0\uACFC\uAC12\uC744 table.pack\uC73C\uB85C \uC2F8\uC11C
3034
+ -- \uD14C\uC774\uBE14\uC744 \uBC18\uD658\uD55C\uB2E4 \u2014 dispatch \uB8E8\uD504\uB294 \uBC18\uD658\uAC12\uC774 \uC22B\uC790\uAC00 \uC544\uB2C8\uBA74 \uC774\uAC78 \uD568\uC218 \uC885\uB8CC \uC2E0\uD638\uB85C \uBCF4\uACE0
3035
+ -- table.unpack(next, 1, next.n)\uC73C\uB85C \uD480\uC5B4 \uCD5C\uC885 \uBC18\uD658\uD55C\uB2E4. table.pack\uC774 n\uC744 \uCC44\uC6CC \uC8FC\uBBC0\uB85C
3036
+ -- \uC911\uAC04 nil\uC774 \uC788\uC5B4\uB3C4 \uBC18\uD658 \uAC1C\uC218\uAC00 \uC815\uD655\uD788 \uBCF4\uC874\uB41C\uB2E4.
2942
3037
  ${N.handlers}[${op(28 /* RETURN */)}] = function(${N.frame}, pc, instr)
2943
3038
  local a, b = instr[2], instr[3]
2944
3039
  if b == 0 then
2945
- return table.unpack(${N.frame}.${N.R}, a, (${N.frame}.${N.multiTop} or (a + 1)) - 1)
3040
+ return table.pack(table.unpack(${N.frame}.${N.R}, a, (${N.frame}.${N.multiTop} or (a + 1)) - 1))
2946
3041
  end
2947
- return table.unpack(${N.frame}.${N.R}, a, a + b - 2)
3042
+ return table.pack(table.unpack(${N.frame}.${N.R}, a, a + b - 2))
2948
3043
  end
2949
3044
 
2950
3045
  -- FORPREP
2951
3046
  ${N.handlers}[${op(29 /* FORPREP */)}] = function(${N.frame}, pc, instr)
2952
3047
  local a = instr[2]
2953
3048
  ${N.frame}.${N.R}[a] = ${N.frame}.${N.R}[a] - ${N.frame}.${N.R}[a + 2]
2954
- return ${N.dispatch}(${N.frame}, pc + instr[3])
3049
+ return pc + instr[3]
2955
3050
  end
2956
3051
 
2957
3052
  -- FORLOOP
@@ -2967,9 +3062,9 @@ ${N.handlers}[${op(30 /* FORLOOP */)}] = function(${N.frame}, pc, instr)
2967
3062
  end
2968
3063
  if ok then
2969
3064
  ${N.frame}.${N.R}[a + 3] = ${N.frame}.${N.R}[a]
2970
- return ${N.dispatch}(${N.frame}, pc + instr[3])
3065
+ return pc + instr[3]
2971
3066
  end
2972
- return ${N.dispatch}(${N.frame}, pc + 1)
3067
+ return pc + 1
2973
3068
  end
2974
3069
 
2975
3070
  -- CLOSURE
@@ -2989,7 +3084,7 @@ ${N.handlers}[${op(31 /* CLOSURE */)}] = function(${N.frame}, pc, instr)
2989
3084
  ${N.frame}.${N.R}[instr[2]] = function(...)
2990
3085
  return ${N.execute}(childProto, capturedUpvals, ...)
2991
3086
  end
2992
- return ${N.dispatch}(${N.frame}, pc + 1 + n)
3087
+ return pc + 1 + n
2993
3088
  end
2994
3089
 
2995
3090
  -- VARARG
@@ -3001,7 +3096,7 @@ ${N.handlers}[${op(32 /* VARARG */)}] = function(${N.frame}, pc, instr)
3001
3096
  if instr[3] == 0 then
3002
3097
  ${N.frame}.${N.multiTop} = instr[2] + count
3003
3098
  end
3004
- return ${N.dispatch}(${N.frame}, pc + 1)
3099
+ return pc + 1
3005
3100
  end
3006
3101
 
3007
3102
  -- SETLIST
@@ -3014,7 +3109,7 @@ ${N.handlers}[${op(33 /* SETLIST */)}] = function(${N.frame}, pc, instr)
3014
3109
  for i = 0, n - 1 do
3015
3110
  tbl[startIdx + i] = ${N.frame}.${N.R}[valuesBase + i]
3016
3111
  end
3017
- return ${N.dispatch}(${N.frame}, pc + 1)
3112
+ return pc + 1
3018
3113
  end
3019
3114
 
3020
3115
  function ${N.execute}(proto, upvals, ...)
@@ -3101,20 +3196,22 @@ function markRuntimeNumbersAsStructural(body) {
3101
3196
  });
3102
3197
  }
3103
3198
  function runVmify(program, options) {
3104
- const globalNames = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS;
3199
+ const builtinGlobals = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS;
3105
3200
  const random = options.random ?? Math.random;
3201
+ const debug = options.debug ?? false;
3106
3202
  const opcodeMap = createOpcodeMap(random);
3107
3203
  const names = generateVmNames(random);
3108
- const compiler = new VmCompiler(program, globalNames, opcodeMap);
3204
+ const compiler = new VmCompiler(program, builtinGlobals, opcodeMap, debug);
3109
3205
  const topProto = compiler.compile();
3206
+ const usedGlobalNames = compiler.getUsedGlobalNames();
3110
3207
  const globalsTable = table(
3111
- globalNames.map((name) => namedField(name, identifier(name)))
3208
+ usedGlobalNames.map((name) => namedField(name, identifier(name)))
3112
3209
  );
3113
- const runtimeSource = buildVmRuntimeSource(names, opcodeMap);
3210
+ const runtimeSource = buildVmRuntimeSource(names, opcodeMap, debug);
3114
3211
  const runtimeProgram = import_luau_parser4.luauparser.parse(runtimeSource);
3115
3212
  const runtimeStatements = runtimeProgram.body.statements;
3116
3213
  markRuntimeNumbersAsStructural(runtimeProgram.body);
3117
- const protoLiteral = serializeProto(topProto, names);
3214
+ const protoLiteral = serializeProto(topProto, names, debug);
3118
3215
  const newBody = [
3119
3216
  localStatement(names.globals, globalsTable),
3120
3217
  ...runtimeStatements,
package/dist/index.d.cts CHANGED
@@ -47,12 +47,12 @@ interface EncryptNumbersOptions {
47
47
 
48
48
  interface VmifyOptions {
49
49
  /**
50
- * VM 밖에서 값을 그대로 참조해야 하는 전역 이름 목록(GETGLOBAL/SETGLOBAL 대상).
51
- * Luau엔 getfenv가 없어서 "임의의 전역 읽기"를 흉내낼 수 없기 때문에,
52
- * 여기 나열된 이름들만 브리지 테이블에 실제 값으로 미리 채워 넣는다.
53
- * 나열되지 않은 전역을 참조하면 컴파일은 되지만 런타임에 nil이 나온다 — 실사용 전
54
- * 반드시 프로그램에서 실제로 쓰는 전역 이름을 전부 이 목록에 채워야 함(TODO: 컴파일러가
55
- * ScopeAnalysis.globalsByName을 이용해 자동으로 목록을 뽑아주도록 개선 가능).
50
+ * 스코프 분석에 미리 등록해둘 전역 이름 시드 목록. 여기 없는 이름이라도 프로그램에서
51
+ * 실제로 참조되면(로컬/파라미터/업밸류로 안 풀리는 식별자는 전부) 스코프 분석이
52
+ * 자동으로 전역으로 잡아내므로, 브릿지 테이블은 이 옵션이 아니라
53
+ * VmCompiler.getUsedGlobalNames()(실제 사용된 전역 전체)로 만든다 — 옛날엔 이 목록에
54
+ * 없는 이름을 쓰면 컴파일은 되는데 GETGLOBAL이 조용히 nil을 반환해서
55
+ * "attempt to call a nil value"로 터졌었음.
56
56
  */
57
57
  builtinGlobals?: readonly string[];
58
58
  /**
@@ -60,6 +60,13 @@ interface VmifyOptions {
60
60
  * Math.random. 재현 가능한 빌드가 필요하면(테스트 등) 시드 고정 PRNG를 넘길 것.
61
61
  */
62
62
  random?: () => number;
63
+ /**
64
+ * 디버그 빌드. 켜면 각 인스트럭션에 원본 소스 줄 번호를, CALL에는 호출 대상 이름을
65
+ * 함께 직렬화하고, 런타임 CALL 핸들러가 non-callable 값을 부르려 할 때 그냥
66
+ * "attempt to call a nil value"가 아니라 "[vmdebug] ... '대상이름' (source line N)"으로
67
+ * 원본 위치를 찍어준다. 산출물이 조금 커지고 VM 시그니처가 노출되므로 진단용으로만 쓸 것.
68
+ */
69
+ debug?: boolean;
63
70
  }
64
71
 
65
72
  type ObfuscateConfigEach<C> = ({