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.d.ts 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> = ({
package/dist/index.js CHANGED
@@ -566,7 +566,7 @@ function splitByteChunks(bytes, min, max) {
566
566
  return chunks.length > 0 ? chunks : [bytes];
567
567
  }
568
568
  function stringCharCall(bytes) {
569
- return call(member(identifier("string"), "char"), bytes.map(numberLiteral));
569
+ return call(member(identifier("string"), "char"), bytes.map(vmNumberLiteral));
570
570
  }
571
571
  function buildConcatChain(chunks) {
572
572
  function build(lo, hi) {
@@ -597,7 +597,7 @@ function buildShuffledTables(chunks) {
597
597
  for (let slot = 0; slot < n; slot++) {
598
598
  order[storageOrder[slot]] = slot + 1;
599
599
  }
600
- const orderFields = order.map((slot) => positionalField(numberLiteral(slot)));
600
+ const orderFields = order.map((slot) => positionalField(vmNumberLiteral(slot)));
601
601
  return {
602
602
  storeVar,
603
603
  orderVar,
@@ -616,7 +616,7 @@ function buildForLoopStringExpr(chunks) {
616
616
  localStatement(accVar, stringLiteral("")),
617
617
  numericForStatement(
618
618
  iVar,
619
- numberLiteral(1),
619
+ vmNumberLiteral(1),
620
620
  unary("#", identifier(orderVar)),
621
621
  block([
622
622
  assignmentStatement(
@@ -648,7 +648,7 @@ function buildRecursiveStringExpr(chunks) {
648
648
  ]),
649
649
  returnStatement([
650
650
  call(identifier(recName), [
651
- binary("+", identifier(iParam), numberLiteral(1)),
651
+ binary("+", identifier(iParam), vmNumberLiteral(1)),
652
652
  binary(
653
653
  "..",
654
654
  identifier(accParam),
@@ -660,7 +660,7 @@ function buildRecursiveStringExpr(chunks) {
660
660
  return iife([
661
661
  ...stmts,
662
662
  localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
663
- returnStatement([call(identifier(recName), [numberLiteral(1), stringLiteral("")])])
663
+ returnStatement([call(identifier(recName), [vmNumberLiteral(1), stringLiteral("")])])
664
664
  ]);
665
665
  }
666
666
  function pickControlFlowStrategy() {
@@ -1738,9 +1738,10 @@ var DEFAULT_BUILTIN_GLOBALS = [
1738
1738
  "next"
1739
1739
  ];
1740
1740
  var VmCompiler = class {
1741
- constructor(program, builtinGlobals = DEFAULT_BUILTIN_GLOBALS, opcodeMap) {
1741
+ constructor(program, builtinGlobals = DEFAULT_BUILTIN_GLOBALS, opcodeMap, debug = false) {
1742
1742
  this.program = program;
1743
1743
  this.opcodeMap = opcodeMap;
1744
+ this.debug = debug;
1744
1745
  this.analysis = luauparser.analyzeScopes(program, { builtinGlobals });
1745
1746
  this.enclosing = buildEnclosingFunctionMap(program);
1746
1747
  for (const [id, binding] of this.analysis.bindings) {
@@ -1750,11 +1751,29 @@ var VmCompiler = class {
1750
1751
  }
1751
1752
  program;
1752
1753
  opcodeMap;
1754
+ debug;
1753
1755
  analysis;
1754
1756
  enclosing;
1755
1757
  captured = /* @__PURE__ */ new Set();
1756
1758
  protoIdCounter = 0;
1757
1759
  declToBinding = /* @__PURE__ */ new Map();
1760
+ /**
1761
+ * 실제로 프로그램이 참조하는 모든 전역 이름(사전 등록된 builtinGlobals +
1762
+ * 스코프 분석이 자동으로 찾아낸, 로컬/파라미터/업밸류로 안 풀리는 나머지 식별자
1763
+ * 전부)을 돌려준다. Vmify.ts가 이 목록으로 브릿지 테이블(globals)을 만든다.
1764
+ *
1765
+ * 예전엔 호출자가 넘긴 builtinGlobals 목록만 브릿지에 넣었는데, 그 목록에
1766
+ * 없는 전역(예: Roblox API나 실행기 전용 전역을 다 못 채운 경우)을 참조하면
1767
+ * 컴파일은 성공하지만 GETGLOBAL이 조용히 nil을 반환해서 "attempt to call a
1768
+ * nil value"로 터진다 — 게다가 원인이 VM 디스패치 쪽 스택에서만 보여서
1769
+ * "opcode 매핑이 깨졌나?" 하고 엉뚱한 곳을 의심하게 만든다. analyzeScopes는
1770
+ * 어차피 로컬/업밸류로 못 푸는 식별자를 전부 global 바인딩으로 잡아두므로,
1771
+ * builtinGlobals에 없던 이름도 여기 globalsByName에는 다 들어있다 —
1772
+ * 하드코딩된 허용목록 대신 이걸 그대로 쓰면 커버리지 문제 자체가 사라진다.
1773
+ */
1774
+ getUsedGlobalNames() {
1775
+ return Array.from(this.analysis.globalsByName.keys());
1776
+ }
1758
1777
  computeCaptured() {
1759
1778
  for (const binding of this.analysis.bindings.values()) {
1760
1779
  if (binding.kind === "global") continue;
@@ -1791,9 +1810,39 @@ var VmCompiler = class {
1791
1810
  }
1792
1811
  emit(state, op, a, b, c, comment) {
1793
1812
  const physicalOp = this.opcodeMap ? this.opcodeMap[op] : op;
1794
- state.proto.code.push({ op: physicalOp, a, b, c, comment });
1813
+ state.proto.code.push({ op: physicalOp, a, b, c, comment, dbgLine: this.debug ? state.curLine : void 0 });
1795
1814
  return state.proto.code.length - 1;
1796
1815
  }
1816
+ /** debug 빌드: 방금 emit한 CALL 인스트럭션에 "호출 대상 표현식 문자열"과 그 소스 줄을
1817
+ * 박아넣는다(런타임이 non-callable을 잡았을 때 사람이 읽을 위치를 찍게). */
1818
+ stampCall(state, idx, callExpr, calleeExpr) {
1819
+ if (!this.debug) return;
1820
+ const instr = state.proto.code[idx];
1821
+ instr.dbgName = this.describeExpr(calleeExpr);
1822
+ const line = calleeExpr.line ?? callExpr.line;
1823
+ if (line) instr.dbgLine = line.start;
1824
+ }
1825
+ /** 표현식을 사람이 읽을 짧은 문자열로. 위치 추적용이라 완벽할 필요는 없음. */
1826
+ describeExpr(e) {
1827
+ switch (e.type) {
1828
+ case "Identifier":
1829
+ return e.name;
1830
+ case "MemberExpression":
1831
+ return `${this.describeExpr(e.object)}.${e.property.name}`;
1832
+ case "MethodCallExpression":
1833
+ return `${this.describeExpr(e.object)}:${e.method.name}(...)`;
1834
+ case "IndexExpression":
1835
+ return `${this.describeExpr(e.object)}[...]`;
1836
+ case "CallExpression":
1837
+ return `${this.describeExpr(e.callee)}(...)`;
1838
+ case "ParenthesizedExpression":
1839
+ return `(${this.describeExpr(e.expression)})`;
1840
+ case "StringLiteral":
1841
+ return `"${String(e.value).slice(0, 20)}"`;
1842
+ default:
1843
+ return `<${e.type}>`;
1844
+ }
1845
+ }
1797
1846
  konst(state, value) {
1798
1847
  return internConst(state.proto, value);
1799
1848
  }
@@ -1885,6 +1934,7 @@ var VmCompiler = class {
1885
1934
  state.regs.releaseLocalsTo(saved);
1886
1935
  }
1887
1936
  compileStatement(stmt, state) {
1937
+ if (this.debug && stmt.line) state.curLine = stmt.line.start;
1888
1938
  switch (stmt.type) {
1889
1939
  case "LocalStatement": {
1890
1940
  const bindingIds = stmt.names.map((n) => this.bindingIdOfDecl(n));
@@ -1910,11 +1960,26 @@ var VmCompiler = class {
1910
1960
  return;
1911
1961
  }
1912
1962
  case "AssignmentStatement": {
1913
- const valueRegs = stmt.values.map((e) => this.compileExpr(e, state));
1914
- stmt.targets.forEach((target, i) => {
1915
- const srcReg = valueRegs[i] ?? valueRegs[valueRegs.length - 1];
1916
- this.compileAssignTarget(target, srcReg, state);
1917
- });
1963
+ const targets = stmt.targets;
1964
+ const values = stmt.values;
1965
+ const base = state.regs.top();
1966
+ if (values.length > 0) {
1967
+ for (let i = 0; i < values.length - 1; i++) {
1968
+ this.compileExprTo(values[i], state, base + i);
1969
+ }
1970
+ const lastIdx = values.length - 1;
1971
+ const remaining = Math.max(targets.length - lastIdx, 1);
1972
+ this.compileExprMultiInto(values[lastIdx], state, base + lastIdx, remaining);
1973
+ }
1974
+ const produced = values.length === 0 ? 0 : values.length - 1 + Math.max(targets.length - (values.length - 1), 1);
1975
+ for (let i = produced; i < targets.length; i++) {
1976
+ this.emit(state, 3 /* LOADNIL */, base + i, base + i, 0);
1977
+ }
1978
+ for (let i = 0; i < targets.length; i++) {
1979
+ state.regs.freeTemp(base + targets.length);
1980
+ this.compileAssignTarget(targets[i], base + i, state);
1981
+ }
1982
+ state.regs.freeTemp(base);
1918
1983
  return;
1919
1984
  }
1920
1985
  case "CompoundAssignmentStatement":
@@ -2258,7 +2323,7 @@ var VmCompiler = class {
2258
2323
  compileCallMultiInto(callExpr, state, wantCount) {
2259
2324
  const base = this.compileExpr(callExpr.callee, state);
2260
2325
  this.compileArgsContiguous(callExpr.arguments, state, base + 1);
2261
- this.emit(state, 27 /* CALL */, base, callExpr.arguments.length + 1, wantCount + 1);
2326
+ this.stampCall(state, this.emit(state, 27 /* CALL */, base, callExpr.arguments.length + 1, wantCount + 1), callExpr, callExpr.callee);
2262
2327
  return base;
2263
2328
  }
2264
2329
  loadConst(state, value) {
@@ -2309,7 +2374,7 @@ var VmCompiler = class {
2309
2374
  if (expr.type === "CallExpression") {
2310
2375
  const calleeReg = this.compileExpr(expr.callee, state);
2311
2376
  const b = this.compileCallArgsAndGetB(expr.arguments, state, calleeReg + 1);
2312
- this.emit(state, 27 /* CALL */, calleeReg, b, wantCount + 1);
2377
+ this.stampCall(state, this.emit(state, 27 /* CALL */, calleeReg, b, wantCount + 1), expr, expr.callee);
2313
2378
  for (let i = 0; i < wantCount; i++) {
2314
2379
  if (calleeReg + i !== base + i) this.emit(state, 0 /* MOVE */, base + i, calleeReg + i, 0);
2315
2380
  }
@@ -2324,7 +2389,7 @@ var VmCompiler = class {
2324
2389
  state.regs.allocTemp();
2325
2390
  const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2);
2326
2391
  const b = argB === 0 ? 0 : expr.arguments.length + 2;
2327
- this.emit(state, 27 /* CALL */, fnSlot, b, wantCount + 1);
2392
+ this.stampCall(state, this.emit(state, 27 /* CALL */, fnSlot, b, wantCount + 1), expr, expr);
2328
2393
  for (let i = 0; i < wantCount; i++) {
2329
2394
  if (fnSlot + i !== base + i) this.emit(state, 0 /* MOVE */, base + i, fnSlot + i, 0);
2330
2395
  }
@@ -2350,7 +2415,7 @@ var VmCompiler = class {
2350
2415
  if (calleeReg !== base) this.emit(state, 0 /* MOVE */, base, calleeReg, 0);
2351
2416
  state.regs.freeTemp(base + 1);
2352
2417
  const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1);
2353
- this.emit(state, 27 /* CALL */, base, b, 0);
2418
+ this.stampCall(state, this.emit(state, 27 /* CALL */, base, b, 0), expr, expr.callee);
2354
2419
  return;
2355
2420
  }
2356
2421
  if (expr.type === "MethodCallExpression") {
@@ -2366,7 +2431,7 @@ var VmCompiler = class {
2366
2431
  state.regs.freeTemp(base + 2);
2367
2432
  const argB = this.compileCallArgsAndGetB(expr.arguments, state, base + 2);
2368
2433
  const b = argB === 0 ? 0 : expr.arguments.length + 2;
2369
- this.emit(state, 27 /* CALL */, base, b, 0);
2434
+ this.stampCall(state, this.emit(state, 27 /* CALL */, base, b, 0), expr, expr);
2370
2435
  return;
2371
2436
  }
2372
2437
  if (expr.type === "VarargExpression") {
@@ -2393,6 +2458,7 @@ var VmCompiler = class {
2393
2458
  return args.length + 1;
2394
2459
  }
2395
2460
  compileExpr(expr, state, discard = false) {
2461
+ if (this.debug && expr.line) state.curLine = expr.line.start;
2396
2462
  switch (expr.type) {
2397
2463
  case "NilLiteral": {
2398
2464
  const r = state.regs.allocTemp();
@@ -2550,7 +2616,8 @@ var VmCompiler = class {
2550
2616
  case "CallExpression": {
2551
2617
  const base = this.compileExpr(expr.callee, state);
2552
2618
  const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1);
2553
- this.emit(state, 27 /* CALL */, base, b, discard ? 1 : 2);
2619
+ const callIdx = this.emit(state, 27 /* CALL */, base, b, discard ? 1 : 2);
2620
+ this.stampCall(state, callIdx, expr, expr.callee);
2554
2621
  state.regs.freeTemp(base + (discard ? 0 : 1));
2555
2622
  return base;
2556
2623
  }
@@ -2562,7 +2629,8 @@ var VmCompiler = class {
2562
2629
  state.regs.allocTemp();
2563
2630
  const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2);
2564
2631
  const b = argB === 0 ? 0 : expr.arguments.length + 2;
2565
- this.emit(state, 27 /* CALL */, fnSlot, b, discard ? 1 : 2);
2632
+ const callIdx = this.emit(state, 27 /* CALL */, fnSlot, b, discard ? 1 : 2);
2633
+ this.stampCall(state, callIdx, expr, expr);
2566
2634
  state.regs.freeTemp(fnSlot + (discard ? 0 : 1));
2567
2635
  return fnSlot;
2568
2636
  }
@@ -2675,15 +2743,20 @@ function serializeConstValue(v) {
2675
2743
  if (typeof v === "number") return numberLiteral(v);
2676
2744
  return booleanLiteral(v);
2677
2745
  }
2678
- function serializeInstrs(proto) {
2679
- const fields = proto.code.map((instr) => positionalField(
2680
- table([
2746
+ function serializeInstrs(proto, debug) {
2747
+ const fields = proto.code.map((instr) => {
2748
+ const tuple = [
2681
2749
  positionalField(vmNumberLiteral(instr.op)),
2682
2750
  positionalField(vmNumberLiteral(instr.a)),
2683
2751
  positionalField(vmNumberLiteral(instr.b)),
2684
2752
  positionalField(vmNumberLiteral(instr.c))
2685
- ])
2686
- ));
2753
+ ];
2754
+ if (debug) {
2755
+ tuple.push(positionalField(numberLiteral(instr.dbgLine ?? 0)));
2756
+ tuple.push(positionalField(stringLiteral(instr.dbgName ?? "")));
2757
+ }
2758
+ return positionalField(table(tuple));
2759
+ });
2687
2760
  return table(fields);
2688
2761
  }
2689
2762
  function serializeUpvalDescs(proto, names) {
@@ -2695,29 +2768,41 @@ function serializeUpvalDescs(proto, names) {
2695
2768
  ));
2696
2769
  return table(fields);
2697
2770
  }
2698
- function serializeProto(proto, names) {
2771
+ function serializeProto(proto, names, debug = false) {
2699
2772
  const fields = [
2700
2773
  namedField(names.numParams, vmNumberLiteral(proto.numParams)),
2701
2774
  namedField(names.hasVarargs, booleanLiteral(proto.hasVarargs)),
2702
2775
  namedField(names.maxRegs, vmNumberLiteral(proto.maxRegs)),
2703
- namedField(names.code, serializeInstrs(proto)),
2776
+ namedField(names.code, serializeInstrs(proto, debug)),
2704
2777
  namedField(names.consts, serializeConsts(proto.consts)),
2705
2778
  namedField(names.upvalDescs, serializeUpvalDescs(proto, names)),
2706
- namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names)))))
2779
+ namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names, debug)))))
2707
2780
  ];
2708
2781
  return table(fields);
2709
2782
  }
2710
2783
 
2711
2784
  // src/passes/vmify/runtime.ts
2712
- function buildVmRuntimeSource(names, opcodeMap) {
2785
+ function buildVmRuntimeSource(names, opcodeMap, debug = false) {
2713
2786
  const N = names;
2714
2787
  const op = (o) => opcodeMap[o];
2715
2788
  return `
2716
2789
  local ${N.handlers} = {}
2717
2790
 
2791
+ -- \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
2792
+ -- \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
2793
+ -- \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
2794
+ -- \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.
2718
2795
  local function ${N.dispatch}(${N.frame}, pc)
2719
- local instr = ${N.frame}.${N.code}[pc]
2720
- return ${N.handlers}[instr[1]](${N.frame}, pc, instr)
2796
+ local ${N.handlers} = ${N.handlers}
2797
+ while true do
2798
+ local instr = ${N.frame}.${N.code}[pc]
2799
+ local next = ${N.handlers}[instr[1]](${N.frame}, pc, instr)
2800
+ if type(next) == "number" then
2801
+ pc = next
2802
+ else
2803
+ return table.unpack(next, 1, next.n)
2804
+ end
2805
+ end
2721
2806
  end
2722
2807
 
2723
2808
  local function ${N.rk}(${N.frame}, x)
@@ -2731,67 +2816,67 @@ end
2731
2816
  -- MOVE
2732
2817
  ${N.handlers}[${op(0 /* MOVE */)}] = function(${N.frame}, pc, instr)
2733
2818
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.R}[instr[3]]
2734
- return ${N.dispatch}(${N.frame}, pc + 1)
2819
+ return pc + 1
2735
2820
  end
2736
2821
 
2737
2822
  -- LOADK
2738
2823
  ${N.handlers}[${op(1 /* LOADK */)}] = function(${N.frame}, pc, instr)
2739
2824
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.K}[instr[3] + 1]
2740
- return ${N.dispatch}(${N.frame}, pc + 1)
2825
+ return pc + 1
2741
2826
  end
2742
2827
 
2743
2828
  -- LOADBOOL
2744
2829
  ${N.handlers}[${op(2 /* LOADBOOL */)}] = function(${N.frame}, pc, instr)
2745
2830
  ${N.frame}.${N.R}[instr[2]] = (instr[3] ~= 0)
2746
- return ${N.dispatch}(${N.frame}, pc + 1)
2831
+ return pc + 1
2747
2832
  end
2748
2833
 
2749
2834
  -- LOADNIL
2750
2835
  ${N.handlers}[${op(3 /* LOADNIL */)}] = function(${N.frame}, pc, instr)
2751
2836
  for i = instr[2], instr[3] do ${N.frame}.${N.R}[i] = nil end
2752
- return ${N.dispatch}(${N.frame}, pc + 1)
2837
+ return pc + 1
2753
2838
  end
2754
2839
 
2755
2840
  -- GETUPVAL
2756
2841
  ${N.handlers}[${op(4 /* GETUPVAL */)}] = function(${N.frame}, pc, instr)
2757
2842
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.upvals}[instr[3] + 1]
2758
- return ${N.dispatch}(${N.frame}, pc + 1)
2843
+ return pc + 1
2759
2844
  end
2760
2845
 
2761
2846
  -- SETUPVAL
2762
2847
  ${N.handlers}[${op(5 /* SETUPVAL */)}] = function(${N.frame}, pc, instr)
2763
2848
  ${N.frame}.${N.upvals}[instr[3] + 1] = ${N.frame}.${N.R}[instr[2]]
2764
- return ${N.dispatch}(${N.frame}, pc + 1)
2849
+ return pc + 1
2765
2850
  end
2766
2851
 
2767
2852
  -- GETGLOBAL (b\uB294 RK\uAC00 \uC544\uB2C8\uB77C \uC0C1\uC218\uD480 \uC21C\uC218 \uC778\uB371\uC2A4)
2768
2853
  ${N.handlers}[${op(6 /* GETGLOBAL */)}] = function(${N.frame}, pc, instr)
2769
2854
  ${N.frame}.${N.R}[instr[2]] = ${N.globals}[${N.frame}.${N.K}[instr[3] + 1]]
2770
- return ${N.dispatch}(${N.frame}, pc + 1)
2855
+ return pc + 1
2771
2856
  end
2772
2857
 
2773
2858
  -- SETGLOBAL
2774
2859
  ${N.handlers}[${op(7 /* SETGLOBAL */)}] = function(${N.frame}, pc, instr)
2775
2860
  ${N.globals}[${N.frame}.${N.K}[instr[3] + 1]] = ${N.frame}.${N.R}[instr[2]]
2776
- return ${N.dispatch}(${N.frame}, pc + 1)
2861
+ return pc + 1
2777
2862
  end
2778
2863
 
2779
2864
  -- GETTABLE
2780
2865
  ${N.handlers}[${op(8 /* GETTABLE */)}] = function(${N.frame}, pc, instr)
2781
2866
  ${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.R}[instr[3]][${N.rk}(${N.frame}, instr[4])]
2782
- return ${N.dispatch}(${N.frame}, pc + 1)
2867
+ return pc + 1
2783
2868
  end
2784
2869
 
2785
2870
  -- SETTABLE
2786
2871
  ${N.handlers}[${op(9 /* SETTABLE */)}] = function(${N.frame}, pc, instr)
2787
2872
  ${N.frame}.${N.R}[instr[2]][${N.rk}(${N.frame}, instr[3])] = ${N.rk}(${N.frame}, instr[4])
2788
- return ${N.dispatch}(${N.frame}, pc + 1)
2873
+ return pc + 1
2789
2874
  end
2790
2875
 
2791
2876
  -- NEWTABLE
2792
2877
  ${N.handlers}[${op(10 /* NEWTABLE */)}] = function(${N.frame}, pc, instr)
2793
2878
  ${N.frame}.${N.R}[instr[2]] = {}
2794
- return ${N.dispatch}(${N.frame}, pc + 1)
2879
+ return pc + 1
2795
2880
  end
2796
2881
 
2797
2882
  -- SELF (R[a] = R[b][key]; R[a+1] = R[b])
@@ -2799,90 +2884,90 @@ ${N.handlers}[${op(11 /* SELF */)}] = function(${N.frame}, pc, instr)
2799
2884
  local obj = ${N.frame}.${N.R}[instr[3]]
2800
2885
  ${N.frame}.${N.R}[instr[2]] = obj[${N.rk}(${N.frame}, instr[4])]
2801
2886
  ${N.frame}.${N.R}[instr[2] + 1] = obj
2802
- return ${N.dispatch}(${N.frame}, pc + 1)
2887
+ return pc + 1
2803
2888
  end
2804
2889
 
2805
2890
  -- ADD SUB MUL DIV MOD POW
2806
2891
  ${N.handlers}[${op(12 /* ADD */)}] = function(${N.frame}, pc, instr)
2807
2892
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) + ${N.rk}(${N.frame}, instr[4])
2808
- return ${N.dispatch}(${N.frame}, pc + 1)
2893
+ return pc + 1
2809
2894
  end
2810
2895
  ${N.handlers}[${op(13 /* SUB */)}] = function(${N.frame}, pc, instr)
2811
2896
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) - ${N.rk}(${N.frame}, instr[4])
2812
- return ${N.dispatch}(${N.frame}, pc + 1)
2897
+ return pc + 1
2813
2898
  end
2814
2899
  ${N.handlers}[${op(14 /* MUL */)}] = 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
  ${N.handlers}[${op(15 /* DIV */)}] = function(${N.frame}, pc, instr)
2819
2904
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) / ${N.rk}(${N.frame}, instr[4])
2820
- return ${N.dispatch}(${N.frame}, pc + 1)
2905
+ return pc + 1
2821
2906
  end
2822
2907
  ${N.handlers}[${op(16 /* MOD */)}] = function(${N.frame}, pc, instr)
2823
2908
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) % ${N.rk}(${N.frame}, instr[4])
2824
- return ${N.dispatch}(${N.frame}, pc + 1)
2909
+ return pc + 1
2825
2910
  end
2826
2911
  ${N.handlers}[${op(17 /* POW */)}] = function(${N.frame}, pc, instr)
2827
2912
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) ^ ${N.rk}(${N.frame}, instr[4])
2828
- return ${N.dispatch}(${N.frame}, pc + 1)
2913
+ return pc + 1
2829
2914
  end
2830
2915
 
2831
2916
  -- IDIV (Luau \uBC14\uB2E5 \uB098\uB217\uC148)
2832
2917
  ${N.handlers}[${op(34 /* IDIV */)}] = function(${N.frame}, pc, instr)
2833
2918
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) // ${N.rk}(${N.frame}, instr[4])
2834
- return ${N.dispatch}(${N.frame}, pc + 1)
2919
+ return pc + 1
2835
2920
  end
2836
2921
 
2837
2922
  -- CONCAT
2838
2923
  ${N.handlers}[${op(18 /* CONCAT */)}] = 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
 
2843
2928
  -- UNM
2844
2929
  ${N.handlers}[${op(19 /* UNM */)}] = function(${N.frame}, pc, instr)
2845
2930
  ${N.frame}.${N.R}[instr[2]] = -${N.frame}.${N.R}[instr[3]]
2846
- return ${N.dispatch}(${N.frame}, pc + 1)
2931
+ return pc + 1
2847
2932
  end
2848
2933
 
2849
2934
  -- NOT
2850
2935
  ${N.handlers}[${op(20 /* NOT */)}] = function(${N.frame}, pc, instr)
2851
2936
  ${N.frame}.${N.R}[instr[2]] = not ${N.frame}.${N.R}[instr[3]]
2852
- return ${N.dispatch}(${N.frame}, pc + 1)
2937
+ return pc + 1
2853
2938
  end
2854
2939
 
2855
2940
  -- LEN
2856
2941
  ${N.handlers}[${op(21 /* LEN */)}] = function(${N.frame}, pc, instr)
2857
2942
  ${N.frame}.${N.R}[instr[2]] = #${N.frame}.${N.R}[instr[3]]
2858
- return ${N.dispatch}(${N.frame}, pc + 1)
2943
+ return pc + 1
2859
2944
  end
2860
2945
 
2861
2946
  -- JMP (pc += a)
2862
2947
  ${N.handlers}[${op(22 /* JMP */)}] = function(${N.frame}, pc, instr)
2863
- return ${N.dispatch}(${N.frame}, pc + instr[2])
2948
+ return pc + instr[2]
2864
2949
  end
2865
2950
 
2866
2951
  -- EQ LT LE (\uAC12 \uC0DD\uC131\uD615 \uBE44\uAD50)
2867
2952
  ${N.handlers}[${op(23 /* EQ */)}] = function(${N.frame}, pc, instr)
2868
2953
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) == ${N.rk}(${N.frame}, instr[4])
2869
- return ${N.dispatch}(${N.frame}, pc + 1)
2954
+ return pc + 1
2870
2955
  end
2871
2956
  ${N.handlers}[${op(24 /* LT */)}] = function(${N.frame}, pc, instr)
2872
2957
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) < ${N.rk}(${N.frame}, instr[4])
2873
- return ${N.dispatch}(${N.frame}, pc + 1)
2958
+ return pc + 1
2874
2959
  end
2875
2960
  ${N.handlers}[${op(25 /* LE */)}] = function(${N.frame}, pc, instr)
2876
2961
  ${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) <= ${N.rk}(${N.frame}, instr[4])
2877
- return ${N.dispatch}(${N.frame}, pc + 1)
2962
+ return pc + 1
2878
2963
  end
2879
2964
 
2880
2965
  -- TEST (if (not R[a]) != (c!=0) then pc += 2 else pc += 1 \u2014 \uBCF4\uD1B5 \uB2E4\uC74C \uBA85\uB839\uC740 JMP)
2881
2966
  ${N.handlers}[${op(26 /* TEST */)}] = function(${N.frame}, pc, instr)
2882
2967
  if (not ${N.frame}.${N.R}[instr[2]]) ~= (instr[4] ~= 0) then
2883
- return ${N.dispatch}(${N.frame}, pc + 2)
2968
+ return pc + 2
2884
2969
  end
2885
- return ${N.dispatch}(${N.frame}, pc + 1)
2970
+ return pc + 1
2886
2971
  end
2887
2972
 
2888
2973
  -- CALL
@@ -2897,7 +2982,13 @@ ${N.handlers}[${op(27 /* CALL */)}] = function(${N.frame}, pc, instr)
2897
2982
  end
2898
2983
  local args = table.create(nargs)
2899
2984
  for i = 1, nargs do args[i] = ${N.frame}.${N.R}[a + i] end
2900
- local rets = table.pack(fn(table.unpack(args, 1, nargs)))
2985
+ ${debug ? ` if type(fn) ~= "function" then
2986
+ local mt = getmetatable(fn)
2987
+ if not (mt and mt.__call) then
2988
+ error(("[vmdebug] attempt to call a %s value: '%s' (source line %d)"):format(type(fn), tostring(instr[6]), tonumber(instr[5]) or -1), 0)
2989
+ end
2990
+ end
2991
+ ` : ""} local rets = table.pack(fn(table.unpack(args, 1, nargs)))
2901
2992
  if c == 1 then
2902
2993
  -- discard
2903
2994
  elseif c == 0 then
@@ -2907,23 +2998,27 @@ ${N.handlers}[${op(27 /* CALL */)}] = function(${N.frame}, pc, instr)
2907
2998
  local want = c - 1
2908
2999
  for i = 1, want do ${N.frame}.${N.R}[a + i - 1] = rets[i] end
2909
3000
  end
2910
- return ${N.dispatch}(${N.frame}, pc + 1)
3001
+ return pc + 1
2911
3002
  end
2912
3003
 
2913
3004
  -- RETURN
3005
+ -- \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
3006
+ -- \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
3007
+ -- 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
3008
+ -- \uC911\uAC04 nil\uC774 \uC788\uC5B4\uB3C4 \uBC18\uD658 \uAC1C\uC218\uAC00 \uC815\uD655\uD788 \uBCF4\uC874\uB41C\uB2E4.
2914
3009
  ${N.handlers}[${op(28 /* RETURN */)}] = function(${N.frame}, pc, instr)
2915
3010
  local a, b = instr[2], instr[3]
2916
3011
  if b == 0 then
2917
- return table.unpack(${N.frame}.${N.R}, a, (${N.frame}.${N.multiTop} or (a + 1)) - 1)
3012
+ return table.pack(table.unpack(${N.frame}.${N.R}, a, (${N.frame}.${N.multiTop} or (a + 1)) - 1))
2918
3013
  end
2919
- return table.unpack(${N.frame}.${N.R}, a, a + b - 2)
3014
+ return table.pack(table.unpack(${N.frame}.${N.R}, a, a + b - 2))
2920
3015
  end
2921
3016
 
2922
3017
  -- FORPREP
2923
3018
  ${N.handlers}[${op(29 /* FORPREP */)}] = function(${N.frame}, pc, instr)
2924
3019
  local a = instr[2]
2925
3020
  ${N.frame}.${N.R}[a] = ${N.frame}.${N.R}[a] - ${N.frame}.${N.R}[a + 2]
2926
- return ${N.dispatch}(${N.frame}, pc + instr[3])
3021
+ return pc + instr[3]
2927
3022
  end
2928
3023
 
2929
3024
  -- FORLOOP
@@ -2939,9 +3034,9 @@ ${N.handlers}[${op(30 /* FORLOOP */)}] = function(${N.frame}, pc, instr)
2939
3034
  end
2940
3035
  if ok then
2941
3036
  ${N.frame}.${N.R}[a + 3] = ${N.frame}.${N.R}[a]
2942
- return ${N.dispatch}(${N.frame}, pc + instr[3])
3037
+ return pc + instr[3]
2943
3038
  end
2944
- return ${N.dispatch}(${N.frame}, pc + 1)
3039
+ return pc + 1
2945
3040
  end
2946
3041
 
2947
3042
  -- CLOSURE
@@ -2961,7 +3056,7 @@ ${N.handlers}[${op(31 /* CLOSURE */)}] = function(${N.frame}, pc, instr)
2961
3056
  ${N.frame}.${N.R}[instr[2]] = function(...)
2962
3057
  return ${N.execute}(childProto, capturedUpvals, ...)
2963
3058
  end
2964
- return ${N.dispatch}(${N.frame}, pc + 1 + n)
3059
+ return pc + 1 + n
2965
3060
  end
2966
3061
 
2967
3062
  -- VARARG
@@ -2973,7 +3068,7 @@ ${N.handlers}[${op(32 /* VARARG */)}] = function(${N.frame}, pc, instr)
2973
3068
  if instr[3] == 0 then
2974
3069
  ${N.frame}.${N.multiTop} = instr[2] + count
2975
3070
  end
2976
- return ${N.dispatch}(${N.frame}, pc + 1)
3071
+ return pc + 1
2977
3072
  end
2978
3073
 
2979
3074
  -- SETLIST
@@ -2986,7 +3081,7 @@ ${N.handlers}[${op(33 /* SETLIST */)}] = function(${N.frame}, pc, instr)
2986
3081
  for i = 0, n - 1 do
2987
3082
  tbl[startIdx + i] = ${N.frame}.${N.R}[valuesBase + i]
2988
3083
  end
2989
- return ${N.dispatch}(${N.frame}, pc + 1)
3084
+ return pc + 1
2990
3085
  end
2991
3086
 
2992
3087
  function ${N.execute}(proto, upvals, ...)
@@ -3073,20 +3168,22 @@ function markRuntimeNumbersAsStructural(body) {
3073
3168
  });
3074
3169
  }
3075
3170
  function runVmify(program, options) {
3076
- const globalNames = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS;
3171
+ const builtinGlobals = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS;
3077
3172
  const random = options.random ?? Math.random;
3173
+ const debug = options.debug ?? false;
3078
3174
  const opcodeMap = createOpcodeMap(random);
3079
3175
  const names = generateVmNames(random);
3080
- const compiler = new VmCompiler(program, globalNames, opcodeMap);
3176
+ const compiler = new VmCompiler(program, builtinGlobals, opcodeMap, debug);
3081
3177
  const topProto = compiler.compile();
3178
+ const usedGlobalNames = compiler.getUsedGlobalNames();
3082
3179
  const globalsTable = table(
3083
- globalNames.map((name) => namedField(name, identifier(name)))
3180
+ usedGlobalNames.map((name) => namedField(name, identifier(name)))
3084
3181
  );
3085
- const runtimeSource = buildVmRuntimeSource(names, opcodeMap);
3182
+ const runtimeSource = buildVmRuntimeSource(names, opcodeMap, debug);
3086
3183
  const runtimeProgram = luauparser2.parse(runtimeSource);
3087
3184
  const runtimeStatements = runtimeProgram.body.statements;
3088
3185
  markRuntimeNumbersAsStructural(runtimeProgram.body);
3089
- const protoLiteral = serializeProto(topProto, names);
3186
+ const protoLiteral = serializeProto(topProto, names, debug);
3090
3187
  const newBody = [
3091
3188
  localStatement(names.globals, globalsTable),
3092
3189
  ...runtimeStatements,