luau-obfuscator 1.0.0 → 1.0.2

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.
Files changed (38) hide show
  1. package/.github/workflows/release.yml +56 -56
  2. package/dist/index.cjs +2186 -95
  3. package/dist/index.d.cts +34 -2
  4. package/dist/index.d.ts +34 -2
  5. package/dist/index.js +2186 -95
  6. package/generated/grammers.luau +13643 -0
  7. package/generated/tests.luau +5567 -0
  8. package/package.json +2 -2
  9. package/scripts/test.js +24 -8
  10. package/{scripts/example.luau → smoketest/grammers.luau} +1174 -1174
  11. package/smoketest/tests.luau +115 -0
  12. package/src/config.ts +2 -1
  13. package/src/passes/ConstantArray.ts +90 -89
  14. package/src/passes/EncryptNumbers.ts +122 -65
  15. package/src/passes/EncryptStrings.ts +162 -82
  16. package/src/passes/GlobalMapping.ts +193 -193
  17. package/src/passes/InsertJunk.ts +184 -184
  18. package/src/passes/Minify.ts +5 -5
  19. package/src/passes/NumbersToExpressions.ts +183 -2
  20. package/src/passes/RenameVariables.ts +2 -9
  21. package/src/passes/StringsToExpressions.ts +195 -6
  22. package/src/passes/StripTypes.ts +191 -191
  23. package/src/passes/Vmify.ts +87 -0
  24. package/src/passes/WrapInFunction.ts +31 -31
  25. package/src/passes/nodeFactory.ts +36 -1
  26. package/src/passes/vmify/chunk.ts +54 -0
  27. package/src/passes/vmify/compiler.ts +1026 -0
  28. package/src/passes/vmify/names.ts +72 -0
  29. package/src/passes/vmify/opcodes.ts +80 -0
  30. package/src/passes/vmify/registers.ts +63 -0
  31. package/src/passes/vmify/runtime.ts +334 -0
  32. package/src/passes/vmify/scope-walk.ts +175 -0
  33. package/src/passes/vmify/serialize.ts +68 -0
  34. package/src/passes/walk.ts +141 -43
  35. package/src/pipeline.ts +5 -6
  36. package/tsconfig.json +20 -20
  37. package/tsup.config.ts +9 -9
  38. package/generated/final.luau +0 -1029
@@ -1,185 +1,185 @@
1
- import type {
2
- Program, Block, Statement, Expression,
3
- } from "luau-parser"
4
- import {
5
- identifier, numberLiteral, binary, localStatement, doStatement, block,
6
- ifStatement, ifClause,
7
- } from "./nodeFactory"
8
-
9
- export interface InsertJunkOptions {
10
- /** 기존 statement 하나 앞에 junk를 끼워넣을 확률 (0~1) */
11
- probability: number
12
- /** 블록 하나당 최대 삽입 개수 */
13
- maxPerBlock: number
14
- }
15
-
16
- function randomInt(min: number, max: number): number {
17
- return Math.floor(Math.random() * (max - min + 1)) + min
18
- }
19
-
20
- function randomName(): string {
21
- return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
22
- }
23
-
24
- function junkNumberExpr(): Expression {
25
- const op = Math.random() < 0.5 ? "+" : "*"
26
- return binary(op, numberLiteral(randomInt(1, 999)), numberLiteral(randomInt(1, 999)))
27
- }
28
-
29
- /**
30
- * 실행돼도 관찰 가능한 부작용이 전혀 없는 더미 statement 하나를 만듦.
31
- * - 매번 새 랜덤 이름을 쓰므로 다른 변수와 충돌하지 않음
32
- * - if 분기 조건이 실제로 참/거짓 어느 쪽이어도 몸통이 쓸모없는 local 선언뿐이라
33
- * 상관없음 (opaque predicate를 엄밀하게 항상 거짓으로 만들 필요가 없음)
34
- */
35
- function buildJunkStatement(): Statement {
36
- const variants: Array<() => Statement> = [
37
- () => localStatement(randomName(), junkNumberExpr()),
38
- () => doStatement(block([localStatement(randomName(), junkNumberExpr())])),
39
- () => ifStatement([
40
- ifClause(
41
- binary("==", numberLiteral(randomInt(1, 999)), numberLiteral(randomInt(1, 999))),
42
- block([localStatement(randomName(), junkNumberExpr())]),
43
- ),
44
- ]),
45
- ]
46
- return variants[randomInt(0, variants.length - 1)]()
47
- }
48
-
49
- export function runInsertJunk(program: Program, options: InsertJunkOptions): void {
50
- function processExpr(expr: Expression): void {
51
- switch (expr.type) {
52
- case "InterpolatedStringExpression":
53
- for (const part of expr.parts) {
54
- if (part.kind === "expression") processExpr(part.expression)
55
- }
56
- return
57
- case "FunctionExpression":
58
- processBlock(expr.func.body)
59
- return
60
- case "TableExpression":
61
- for (const field of expr.fields) {
62
- if (field.type === "TableFieldPositional") processExpr(field.value)
63
- else if (field.type === "TableFieldNamed") processExpr(field.value)
64
- else { processExpr(field.key); processExpr(field.value) }
65
- }
66
- return
67
- case "BinaryExpression":
68
- processExpr(expr.left)
69
- processExpr(expr.right)
70
- return
71
- case "UnaryExpression":
72
- processExpr(expr.argument)
73
- return
74
- case "MemberExpression":
75
- processExpr(expr.object)
76
- return
77
- case "IndexExpression":
78
- processExpr(expr.object)
79
- processExpr(expr.index)
80
- return
81
- case "CallExpression":
82
- processExpr(expr.callee)
83
- expr.arguments.forEach(processExpr)
84
- return
85
- case "MethodCallExpression":
86
- processExpr(expr.object)
87
- expr.arguments.forEach(processExpr)
88
- return
89
- case "ParenthesizedExpression":
90
- processExpr(expr.expression)
91
- return
92
- case "TypeAssertionExpression":
93
- processExpr(expr.expression)
94
- return
95
- case "IfElseExpression":
96
- for (const clause of expr.clauses) {
97
- processExpr(clause.condition)
98
- processExpr(clause.body)
99
- }
100
- processExpr(expr.alternate)
101
- return
102
- default:
103
- return
104
- }
105
- }
106
-
107
- function processStatement(stmt: Statement): void {
108
- switch (stmt.type) {
109
- case "LocalStatement":
110
- stmt.init.forEach(processExpr)
111
- return
112
- case "LocalFunctionStatement":
113
- processBlock(stmt.func.body)
114
- return
115
- case "FunctionDeclarationStatement":
116
- processBlock(stmt.func.body)
117
- return
118
- case "AssignmentStatement":
119
- stmt.targets.forEach(processExpr)
120
- stmt.values.forEach(processExpr)
121
- return
122
- case "CompoundAssignmentStatement":
123
- processExpr(stmt.target)
124
- processExpr(stmt.value)
125
- return
126
- case "CallStatement":
127
- processExpr(stmt.expression)
128
- return
129
- case "DoStatement":
130
- processBlock(stmt.body)
131
- return
132
- case "WhileStatement":
133
- processExpr(stmt.condition)
134
- processBlock(stmt.body)
135
- return
136
- case "RepeatStatement":
137
- processBlock(stmt.body)
138
- processExpr(stmt.condition)
139
- return
140
- case "IfStatement":
141
- for (const clause of stmt.clauses) {
142
- processExpr(clause.condition)
143
- processBlock(clause.body)
144
- }
145
- if (stmt.alternate) processBlock(stmt.alternate)
146
- return
147
- case "NumericForStatement":
148
- processExpr(stmt.start)
149
- processExpr(stmt.end)
150
- if (stmt.step) processExpr(stmt.step)
151
- processBlock(stmt.body)
152
- return
153
- case "GenericForStatement":
154
- stmt.iterators.forEach(processExpr)
155
- processBlock(stmt.body)
156
- return
157
- case "ReturnStatement":
158
- stmt.arguments.forEach(processExpr)
159
- return
160
- default:
161
- return
162
- }
163
- }
164
-
165
- /** 자식 블록들부터 먼저 처리한 뒤(재귀 무한 방지 — 새로 끼워넣는 junk는
166
- * 다시 처리하지 않음), 현재 블록에 junk를 끼워넣음. junk는 기존 statement
167
- * "앞"에만 넣으므로 return/break/continue(항상 블록 맨 끝에만 올 수 있음)
168
- * 뒤에 잘못 삽입될 일이 없음. */
169
- function processBlock(blk: Block): void {
170
- for (const stmt of blk.statements) processStatement(stmt)
171
-
172
- const result: Statement[] = []
173
- let inserted = 0
174
- for (const stmt of blk.statements) {
175
- if (inserted < options.maxPerBlock && Math.random() < options.probability) {
176
- result.push(buildJunkStatement())
177
- inserted++
178
- }
179
- result.push(stmt)
180
- }
181
- blk.statements = result
182
- }
183
-
184
- processBlock(program.body)
1
+ import type {
2
+ Program, Block, Statement, Expression,
3
+ } from "luau-parser"
4
+ import {
5
+ identifier, numberLiteral, binary, localStatement, doStatement, block,
6
+ ifStatement, ifClause,
7
+ } from "./nodeFactory"
8
+
9
+ export interface InsertJunkOptions {
10
+ /** 기존 statement 하나 앞에 junk를 끼워넣을 확률 (0~1) */
11
+ probability: number
12
+ /** 블록 하나당 최대 삽입 개수 */
13
+ maxPerBlock: number
14
+ }
15
+
16
+ function randomInt(min: number, max: number): number {
17
+ return Math.floor(Math.random() * (max - min + 1)) + min
18
+ }
19
+
20
+ function randomName(): string {
21
+ return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
22
+ }
23
+
24
+ function junkNumberExpr(): Expression {
25
+ const op = Math.random() < 0.5 ? "+" : "*"
26
+ return binary(op, numberLiteral(randomInt(1, 999)), numberLiteral(randomInt(1, 999)))
27
+ }
28
+
29
+ /**
30
+ * 실행돼도 관찰 가능한 부작용이 전혀 없는 더미 statement 하나를 만듦.
31
+ * - 매번 새 랜덤 이름을 쓰므로 다른 변수와 충돌하지 않음
32
+ * - if 분기 조건이 실제로 참/거짓 어느 쪽이어도 몸통이 쓸모없는 local 선언뿐이라
33
+ * 상관없음 (opaque predicate를 엄밀하게 항상 거짓으로 만들 필요가 없음)
34
+ */
35
+ function buildJunkStatement(): Statement {
36
+ const variants: Array<() => Statement> = [
37
+ () => localStatement(randomName(), junkNumberExpr()),
38
+ () => doStatement(block([localStatement(randomName(), junkNumberExpr())])),
39
+ () => ifStatement([
40
+ ifClause(
41
+ binary("==", numberLiteral(randomInt(1, 999)), numberLiteral(randomInt(1, 999))),
42
+ block([localStatement(randomName(), junkNumberExpr())]),
43
+ ),
44
+ ]),
45
+ ]
46
+ return variants[randomInt(0, variants.length - 1)]()
47
+ }
48
+
49
+ export function runInsertJunk(program: Program, options: InsertJunkOptions): void {
50
+ function processExpr(expr: Expression): void {
51
+ switch (expr.type) {
52
+ case "InterpolatedStringExpression":
53
+ for (const part of expr.parts) {
54
+ if (part.kind === "expression") processExpr(part.expression)
55
+ }
56
+ return
57
+ case "FunctionExpression":
58
+ processBlock(expr.func.body)
59
+ return
60
+ case "TableExpression":
61
+ for (const field of expr.fields) {
62
+ if (field.type === "TableFieldPositional") processExpr(field.value)
63
+ else if (field.type === "TableFieldNamed") processExpr(field.value)
64
+ else { processExpr(field.key); processExpr(field.value) }
65
+ }
66
+ return
67
+ case "BinaryExpression":
68
+ processExpr(expr.left)
69
+ processExpr(expr.right)
70
+ return
71
+ case "UnaryExpression":
72
+ processExpr(expr.argument)
73
+ return
74
+ case "MemberExpression":
75
+ processExpr(expr.object)
76
+ return
77
+ case "IndexExpression":
78
+ processExpr(expr.object)
79
+ processExpr(expr.index)
80
+ return
81
+ case "CallExpression":
82
+ processExpr(expr.callee)
83
+ expr.arguments.forEach(processExpr)
84
+ return
85
+ case "MethodCallExpression":
86
+ processExpr(expr.object)
87
+ expr.arguments.forEach(processExpr)
88
+ return
89
+ case "ParenthesizedExpression":
90
+ processExpr(expr.expression)
91
+ return
92
+ case "TypeAssertionExpression":
93
+ processExpr(expr.expression)
94
+ return
95
+ case "IfElseExpression":
96
+ for (const clause of expr.clauses) {
97
+ processExpr(clause.condition)
98
+ processExpr(clause.body)
99
+ }
100
+ processExpr(expr.alternate)
101
+ return
102
+ default:
103
+ return
104
+ }
105
+ }
106
+
107
+ function processStatement(stmt: Statement): void {
108
+ switch (stmt.type) {
109
+ case "LocalStatement":
110
+ stmt.init.forEach(processExpr)
111
+ return
112
+ case "LocalFunctionStatement":
113
+ processBlock(stmt.func.body)
114
+ return
115
+ case "FunctionDeclarationStatement":
116
+ processBlock(stmt.func.body)
117
+ return
118
+ case "AssignmentStatement":
119
+ stmt.targets.forEach(processExpr)
120
+ stmt.values.forEach(processExpr)
121
+ return
122
+ case "CompoundAssignmentStatement":
123
+ processExpr(stmt.target)
124
+ processExpr(stmt.value)
125
+ return
126
+ case "CallStatement":
127
+ processExpr(stmt.expression)
128
+ return
129
+ case "DoStatement":
130
+ processBlock(stmt.body)
131
+ return
132
+ case "WhileStatement":
133
+ processExpr(stmt.condition)
134
+ processBlock(stmt.body)
135
+ return
136
+ case "RepeatStatement":
137
+ processBlock(stmt.body)
138
+ processExpr(stmt.condition)
139
+ return
140
+ case "IfStatement":
141
+ for (const clause of stmt.clauses) {
142
+ processExpr(clause.condition)
143
+ processBlock(clause.body)
144
+ }
145
+ if (stmt.alternate) processBlock(stmt.alternate)
146
+ return
147
+ case "NumericForStatement":
148
+ processExpr(stmt.start)
149
+ processExpr(stmt.end)
150
+ if (stmt.step) processExpr(stmt.step)
151
+ processBlock(stmt.body)
152
+ return
153
+ case "GenericForStatement":
154
+ stmt.iterators.forEach(processExpr)
155
+ processBlock(stmt.body)
156
+ return
157
+ case "ReturnStatement":
158
+ stmt.arguments.forEach(processExpr)
159
+ return
160
+ default:
161
+ return
162
+ }
163
+ }
164
+
165
+ /** 자식 블록들부터 먼저 처리한 뒤(재귀 무한 방지 — 새로 끼워넣는 junk는
166
+ * 다시 처리하지 않음), 현재 블록에 junk를 끼워넣음. junk는 기존 statement
167
+ * "앞"에만 넣으므로 return/break/continue(항상 블록 맨 끝에만 올 수 있음)
168
+ * 뒤에 잘못 삽입될 일이 없음. */
169
+ function processBlock(blk: Block): void {
170
+ for (const stmt of blk.statements) processStatement(stmt)
171
+
172
+ const result: Statement[] = []
173
+ let inserted = 0
174
+ for (const stmt of blk.statements) {
175
+ if (inserted < options.maxPerBlock && Math.random() < options.probability) {
176
+ result.push(buildJunkStatement())
177
+ inserted++
178
+ }
179
+ result.push(stmt)
180
+ }
181
+ blk.statements = result
182
+ }
183
+
184
+ processBlock(program.body)
185
185
  }
@@ -1,6 +1,6 @@
1
- export function minifyPrinted(code: string): string {
2
- return code
3
- .replace(/\r\n|\r|\n/g, " ")
4
- .replace(/[ \t]+/g, " ")
5
- .trim()
1
+ export function minifyPrinted(code: string): string {
2
+ return code
3
+ .replace(/\r\n|\r|\n/g, " ")
4
+ .replace(/[ \t]+/g, " ")
5
+ .trim()
6
6
  }
@@ -1,25 +1,48 @@
1
- import type { Program, Expression } from "luau-parser"
1
+ import type { Program, Expression, Statement } from "luau-parser"
2
2
  import { transformExpressions } from "./walk"
3
- import { numberLiteral, binary, paren } from "./nodeFactory"
3
+ import {
4
+ numberLiteral, binary, paren, identifier, call, index, table, positionalField,
5
+ localStatement, localFunctionStatement, functionParam, functionBody, functionExpression,
6
+ block, assignmentStatement, numericForStatement, whileStatement, ifStatement, ifClause,
7
+ returnStatement, unary, vmStructuralNumbers,
8
+ } from "./nodeFactory"
4
9
 
5
10
  export interface NumbersToExpressionsOptions {
6
11
  /** 랜덤 보조항의 최소 절댓값 */
7
12
  min: number
8
13
  /** 랜덤 보조항의 최대 절댓값 */
9
14
  max: number
15
+ /**
16
+ * 0~1. 정수 리터럴을 순수 산술식(depth 얕은 a+b 형태) 대신
17
+ * for/while 루프나 재귀 함수 호출로 "런타임에 조립"할 확률.
18
+ * 단순 트리 순회형 상수 접기(constant folding)는 문(statement)/제어흐름을
19
+ * 해석하지 못하므로 이 경로로 만든 값은 그런 도구로는 복원되지 않는다.
20
+ * 기본값 0.35.
21
+ */
22
+ controlFlowProbability?: number
10
23
  }
11
24
 
12
25
  const MAX_DEPTH = 1
13
26
  const NEST_PROBABILITY = 0.35
27
+ const CONTROL_FLOW_PROBABILITY_DEFAULT = 0.35
14
28
 
15
29
  function randomInt(min: number, max: number): number {
16
30
  return Math.floor(Math.random() * (max - min + 1)) + min
17
31
  }
18
32
 
33
+ function randomName(): string {
34
+ return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
35
+ }
36
+
19
37
  function randSign(): 1 | -1 {
20
38
  return Math.random() < 0.5 ? 1 : -1
21
39
  }
22
40
 
41
+ /** (function() ... end)() — 문(statement)들을 하나의 표현식으로 감싸는 즉시실행 함수. */
42
+ function iife(statements: Statement[]): Expression {
43
+ return call(paren(functionExpression(functionBody([], block(statements)))), [])
44
+ }
45
+
23
46
  type Strategy = "add" | "sub" | "mul"
24
47
 
25
48
  /** value를 min~max 범위 정수로 나눌 수 있으면 mul도 후보에 추가 */
@@ -44,6 +67,7 @@ function operand(n: number, min: number, max: number, depth: number): Expression
44
67
  return numberLiteral(n)
45
68
  }
46
69
 
70
+ /** 얕은 산술식(a+b, a-b, a*b) 하나로 값을 위장 — 기존 방식, 가벼운 기본 경로로 유지. */
47
71
  function buildNumberExpr(value: number, min: number, max: number, depth = 0): Expression {
48
72
  const strategy = pickStrategy(value, min, max)
49
73
  let inner: Expression
@@ -78,9 +102,166 @@ function buildNumberExpr(value: number, min: number, max: number, depth = 0): Ex
78
102
  return paren(inner)
79
103
  }
80
104
 
105
+ /** n개의 정수 조각(합이 정확히 value)으로 분할. 마지막 조각은 나머지를 정확히 채운다. */
106
+ function randomPartition(value: number, n: number, min: number, max: number): number[] {
107
+ const parts: number[] = []
108
+ let remaining = value
109
+ for (let i = 0; i < n - 1; i++) {
110
+ const part = randomInt(min, max) * randSign()
111
+ parts.push(part)
112
+ remaining -= part
113
+ }
114
+ parts.push(remaining)
115
+ return parts
116
+ }
117
+
118
+ /**
119
+ * (function()
120
+ * local steps = { s1, s2, ..., sn }
121
+ * local acc = 0
122
+ * for i = 1, #steps do
123
+ * acc = acc + steps[i]
124
+ * end
125
+ * return acc
126
+ * end)()
127
+ */
128
+ function buildForLoopNumberExpr(value: number, min: number, max: number): Expression {
129
+ const n = randomInt(3, 6)
130
+ const steps = randomPartition(value, n, min, max)
131
+
132
+ const accVar = randomName()
133
+ const stepsVar = randomName()
134
+ const iVar = randomName()
135
+
136
+ const stmts: Statement[] = [
137
+ localStatement(stepsVar, table(steps.map((s) => positionalField(numberLiteral(s))))),
138
+ localStatement(accVar, numberLiteral(0)),
139
+ numericForStatement(
140
+ iVar,
141
+ numberLiteral(1),
142
+ unary("#", identifier(stepsVar)),
143
+ block([
144
+ assignmentStatement(
145
+ [identifier(accVar)],
146
+ [binary("+", identifier(accVar), index(identifier(stepsVar), identifier(iVar)))],
147
+ ),
148
+ ]),
149
+ ),
150
+ returnStatement([identifier(accVar)]),
151
+ ]
152
+
153
+ return iife(stmts)
154
+ }
155
+
156
+ /**
157
+ * (function()
158
+ * local acc = BASE
159
+ * local n = N
160
+ * while n > 0 do
161
+ * acc = acc + STEP
162
+ * n = n - 1
163
+ * end
164
+ * return acc
165
+ * end)()
166
+ */
167
+ function buildWhileLoopNumberExpr(value: number, min: number, max: number): Expression {
168
+ const n = randomInt(3, 6)
169
+ const step = randomInt(min, max) * randSign()
170
+ const base = value - n * step
171
+
172
+ const accVar = randomName()
173
+ const nVar = randomName()
174
+
175
+ const stmts: Statement[] = [
176
+ localStatement(accVar, numberLiteral(base)),
177
+ localStatement(nVar, numberLiteral(n)),
178
+ whileStatement(
179
+ binary(">", identifier(nVar), numberLiteral(0)),
180
+ block([
181
+ assignmentStatement([identifier(accVar)], [binary("+", identifier(accVar), numberLiteral(step))]),
182
+ assignmentStatement([identifier(nVar)], [binary("-", identifier(nVar), numberLiteral(1))]),
183
+ ]),
184
+ ),
185
+ returnStatement([identifier(accVar)]),
186
+ ]
187
+
188
+ return iife(stmts)
189
+ }
190
+
191
+ /**
192
+ * (function()
193
+ * local function <rec>(n)
194
+ * if n <= 0 then
195
+ * return BASE
196
+ * end
197
+ * return STEP + <rec>(n - 1)
198
+ * end
199
+ * return <rec>(N)
200
+ * end)()
201
+ */
202
+ function buildRecursiveNumberExpr(value: number, min: number, max: number): Expression {
203
+ const n = randomInt(3, 6)
204
+ const step = randomInt(min, max) * randSign()
205
+ const base = value - n * step
206
+
207
+ const recName = randomName()
208
+ const nParam = randomName()
209
+
210
+ const body = block([
211
+ ifStatement([
212
+ ifClause(
213
+ binary("<=", identifier(nParam), numberLiteral(0)),
214
+ block([returnStatement([numberLiteral(base)])]),
215
+ ),
216
+ ]),
217
+ returnStatement([
218
+ binary(
219
+ "+",
220
+ numberLiteral(step),
221
+ call(identifier(recName), [binary("-", identifier(nParam), numberLiteral(1))]),
222
+ ),
223
+ ]),
224
+ ])
225
+
226
+ return iife([
227
+ localFunctionStatement(recName, functionBody([functionParam(nParam)], body)),
228
+ returnStatement([call(identifier(recName), [numberLiteral(n)])]),
229
+ ])
230
+ }
231
+
232
+ type ControlFlowStrategy = "forLoop" | "whileLoop" | "recursive"
233
+
234
+ function pickControlFlowStrategy(): ControlFlowStrategy {
235
+ const options: ControlFlowStrategy[] = ["forLoop", "whileLoop", "recursive"]
236
+ return options[randomInt(0, options.length - 1)]
237
+ }
238
+
239
+ function buildControlFlowNumberExpr(value: number, min: number, max: number): Expression {
240
+ switch (pickControlFlowStrategy()) {
241
+ case "forLoop":
242
+ return buildForLoopNumberExpr(value, min, max)
243
+ case "whileLoop":
244
+ return buildWhileLoopNumberExpr(value, min, max)
245
+ case "recursive":
246
+ return buildRecursiveNumberExpr(value, min, max)
247
+ }
248
+ }
249
+
81
250
  export function runNumbersToExpressions(program: Program, options: NumbersToExpressionsOptions): void {
251
+ const controlFlowProbability = options.controlFlowProbability ?? CONTROL_FLOW_PROBABILITY_DEFAULT
252
+
82
253
  transformExpressions(program, (expr) => {
83
254
  if (expr.type !== "NumberLiteral") return
255
+ // Vmify가 만든 op/a/b/c 같은 내부 메타데이터는 건드리지 않는다
256
+ // (개수가 코드 크기에 비례해 폭증하고, 로직은 이미 Vmify가 구조적으로 숨긴 값).
257
+ if (vmStructuralNumbers.has(expr)) return
258
+
259
+ // 루프/재귀 조립은 정수 배분(합이 정확히 일치)에 의존하므로 정수에만 적용하고,
260
+ // 실수/범위를 벗어난 값은 기존 산술식 위장으로 처리한다.
261
+ if (Number.isInteger(expr.value) && Math.random() < controlFlowProbability) {
262
+ return buildControlFlowNumberExpr(expr.value, options.min, options.max)
263
+ }
264
+
84
265
  return buildNumberExpr(expr.value, options.min, options.max)
85
266
  })
86
267
  }
@@ -2,24 +2,17 @@ import type { Program } from "luau-parser"
2
2
  import { analyzeScopes, isGlobal } from "luau-parser"
3
3
 
4
4
  export interface RenameVariablesOptions {
5
- random?: () => string
6
- }
7
-
8
- let counter = 0
9
- function defaultRandomName(): string {
10
- counter += 1
11
- return `_l${counter.toString(36)}`
5
+ random: () => string
12
6
  }
13
7
 
14
8
  export function runRenameVariables(program: Program, options: RenameVariablesOptions): void {
15
9
  const analysis = analyzeScopes(program)
16
- const makeName = options.random ?? defaultRandomName
17
10
 
18
11
  for (const binding of analysis.bindings.values()) {
19
12
  if (isGlobal(binding)) continue
20
13
  if (binding.kind === "self") continue
21
14
 
22
- const newName = makeName()
15
+ const newName = options.random()
23
16
  binding.name = newName
24
17
 
25
18
  if (binding.declarationNode) {