luau-obfuscator 1.0.0 → 1.0.1

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.
@@ -1,18 +1,36 @@
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, call, member, identifier } from "./nodeFactory"
3
+ import {
4
+ numberLiteral, binary, call, member, identifier, table, positionalField, index,
5
+ localStatement, localFunctionStatement, functionParam, functionBody, functionExpression,
6
+ block, ifStatement, ifClause, returnStatement, paren, unary, stringLiteral,
7
+ numericForStatement, assignmentStatement,
8
+ } from "./nodeFactory"
4
9
 
5
10
  export interface StringsToExpressionsOptions {
6
11
  /** 청크 하나에 들어가는 바이트 개수 최소값 */
7
12
  min: number
8
13
  /** 청크 하나에 들어가는 바이트 개수 최대값 */
9
14
  max: number
15
+ /**
16
+ * 0~1. 청크를 소스 순서 그대로 이어붙이는 대신, 순서를 섞어 저장해두고
17
+ * for 루프나 재귀 함수로 원래 순서를 되짚어가며 런타임에 조립할 확률.
18
+ * 소스에 보이는 조각 순서와 실제 결합 순서가 달라지므로, 단순 텍스트 상의
19
+ * concat 체인 스캔만으로는 원문을 복원할 수 없다. 기본값 0.6.
20
+ */
21
+ controlFlowProbability?: number
10
22
  }
11
23
 
24
+ const CONTROL_FLOW_PROBABILITY_DEFAULT = 0.6
25
+
12
26
  function randomInt(min: number, max: number): number {
13
27
  return Math.floor(Math.random() * (max - min + 1)) + min
14
28
  }
15
29
 
30
+ function randomName(): string {
31
+ return "_" + globalThis.crypto.randomUUID().replace(/-/g, "")
32
+ }
33
+
16
34
  /** Luau 소스는 UTF-8 바이트 스트림이므로 charCodeAt이 아니라 실제 UTF-8 바이트로 변환.
17
35
  * Buffer(Node 전용) 대신 표준 TextEncoder 사용 — 브라우저/번들러 환경에서도 동작. */
18
36
  function toUtf8Bytes(value: string): number[] {
@@ -35,6 +53,7 @@ function stringCharCall(bytes: number[]): Expression {
35
53
  return call(member(identifier("string"), "char"), bytes.map(numberLiteral))
36
54
  }
37
55
 
56
+ /** 기존 방식: 청크를 소스에 보이는 순서 그대로 ".."로 이어붙임. */
38
57
  function buildConcatChain(chunks: number[][]): Expression {
39
58
  let expr: Expression = stringCharCall(chunks[0])
40
59
  for (let i = 1; i < chunks.length; i++) {
@@ -43,7 +62,146 @@ function buildConcatChain(chunks: number[][]): Expression {
43
62
  return expr
44
63
  }
45
64
 
65
+ /** [0, n)의 무작위 순열 (Fisher-Yates) */
66
+ function shuffledIndices(n: number): number[] {
67
+ const arr = Array.from({ length: n }, (_, i) => i)
68
+ for (let i = n - 1; i > 0; i--) {
69
+ const j = randomInt(0, i)
70
+ ;[arr[i], arr[j]] = [arr[j], arr[i]]
71
+ }
72
+ return arr
73
+ }
74
+
75
+ /** (function() ... end)() — 문(statement)들을 하나의 표현식으로 감싸는 즉시실행 함수. */
76
+ function iife(statements: Statement[]): Expression {
77
+ return call(paren(functionExpression(functionBody([], block(statements)))), [])
78
+ }
79
+
80
+ /**
81
+ * 청크들을 뒤섞어 store 테이블에 저장하고, 원래 순서를 가리키는 order 테이블을 따로 둔다.
82
+ * store에 나타나는 물리적 순서 == 원문 순서가 아니므로, 소스를 눈으로 읽거나
83
+ * 텍스트 패턴만으로 concat해서는 원문이 나오지 않는다 — order를 통해 런타임에
84
+ * 역참조해야만 올바른 순서가 나온다.
85
+ */
86
+ function buildShuffledTables(chunks: number[][]): { storeVar: string; orderVar: string; stmts: Statement[] } {
87
+ const n = chunks.length
88
+ const storageOrder = shuffledIndices(n) // storageOrder[slot] = 원본 청크 인덱스
89
+ const storeVar = randomName()
90
+ const orderVar = randomName()
91
+
92
+ const storeFields = storageOrder.map((originalIdx) => positionalField(stringCharCall(chunks[originalIdx])))
93
+
94
+ // order[originalIdx] = slot(1-based) — 원본 위치 k에 해당하는 조각이 store의 몇 번째에 있는지
95
+ const order = new Array<number>(n)
96
+ for (let slot = 0; slot < n; slot++) {
97
+ order[storageOrder[slot]] = slot + 1
98
+ }
99
+ const orderFields = order.map((slot) => positionalField(numberLiteral(slot)))
100
+
101
+ return {
102
+ storeVar,
103
+ orderVar,
104
+ stmts: [
105
+ localStatement(storeVar, table(storeFields)),
106
+ localStatement(orderVar, table(orderFields)),
107
+ ],
108
+ }
109
+ }
110
+
111
+ /**
112
+ * (function()
113
+ * local store = { ... 뒤섞인 순서의 string.char(...) 조각들 ... }
114
+ * local order = { ... }
115
+ * local acc = ""
116
+ * for i = 1, #order do
117
+ * acc = acc .. store[order[i]]
118
+ * end
119
+ * return acc
120
+ * end)()
121
+ */
122
+ function buildForLoopStringExpr(chunks: number[][]): Expression {
123
+ const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
124
+ const accVar = randomName()
125
+ const iVar = randomName()
126
+
127
+ return iife([
128
+ ...stmts,
129
+ localStatement(accVar, stringLiteral("")),
130
+ numericForStatement(
131
+ iVar,
132
+ numberLiteral(1),
133
+ unary("#", identifier(orderVar)),
134
+ block([
135
+ assignmentStatement(
136
+ [identifier(accVar)],
137
+ [
138
+ binary(
139
+ "..",
140
+ identifier(accVar),
141
+ index(identifier(storeVar), index(identifier(orderVar), identifier(iVar))),
142
+ ),
143
+ ],
144
+ ),
145
+ ]),
146
+ ),
147
+ returnStatement([identifier(accVar)]),
148
+ ])
149
+ }
150
+
151
+ /**
152
+ * (function()
153
+ * local store = { ... }
154
+ * local order = { ... }
155
+ * local function build(i, acc)
156
+ * if i > #order then
157
+ * return acc
158
+ * end
159
+ * return build(i + 1, acc .. store[order[i]])
160
+ * end
161
+ * return build(1, "")
162
+ * end)()
163
+ */
164
+ function buildRecursiveStringExpr(chunks: number[][]): Expression {
165
+ const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
166
+ const recName = randomName()
167
+ const iParam = randomName()
168
+ const accParam = randomName()
169
+
170
+ const body = block([
171
+ ifStatement([
172
+ ifClause(
173
+ binary(">", identifier(iParam), unary("#", identifier(orderVar))),
174
+ block([returnStatement([identifier(accParam)])]),
175
+ ),
176
+ ]),
177
+ returnStatement([
178
+ call(identifier(recName), [
179
+ binary("+", identifier(iParam), numberLiteral(1)),
180
+ binary(
181
+ "..",
182
+ identifier(accParam),
183
+ index(identifier(storeVar), index(identifier(orderVar), identifier(iParam))),
184
+ ),
185
+ ]),
186
+ ]),
187
+ ])
188
+
189
+ return iife([
190
+ ...stmts,
191
+ localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
192
+ returnStatement([call(identifier(recName), [numberLiteral(1), stringLiteral("")])]),
193
+ ])
194
+ }
195
+
196
+ type ControlFlowStrategy = "forLoop" | "recursive"
197
+
198
+ function pickControlFlowStrategy(): ControlFlowStrategy {
199
+ return Math.random() < 0.5 ? "forLoop" : "recursive"
200
+ }
201
+
46
202
  export function runStringsToExpressions(program: Program, options: StringsToExpressionsOptions): void {
203
+ const controlFlowProbability = options.controlFlowProbability ?? CONTROL_FLOW_PROBABILITY_DEFAULT
204
+
47
205
  transformExpressions(program, (expr) => {
48
206
  if (expr.type !== "StringLiteral") return
49
207
  if (expr.value.length === 0) return
@@ -51,6 +209,13 @@ export function runStringsToExpressions(program: Program, options: StringsToExpr
51
209
  const bytes = toUtf8Bytes(expr.value)
52
210
  const chunks = splitByteChunks(bytes, options.min, options.max)
53
211
 
212
+ // 청크가 하나뿐이면 순서를 흩뜨려도 얻는 게 없으니 그냥 원래 방식대로.
213
+ if (chunks.length > 1 && Math.random() < controlFlowProbability) {
214
+ return pickControlFlowStrategy() === "forLoop"
215
+ ? buildForLoopStringExpr(chunks)
216
+ : buildRecursiveStringExpr(chunks)
217
+ }
218
+
54
219
  return buildConcatChain(chunks)
55
220
  })
56
221
  }
@@ -1,192 +1,192 @@
1
- import type {
2
- Program, Block, Statement, Expression, FunctionBody,
3
- } from "luau-parser"
4
-
5
- /**
6
- * 파이프라인 맨 앞에서 실행되어야 하는 패스.
7
- *
8
- * 이후 패스들(GlobalMapping, RenameVariables 등)은 값 트리만 순회하고
9
- * `typeof(x)`, `x :: T` 같은 타입 노드 내부의 expression은 건드리지 않는다.
10
- * 타입을 나중까지 들고 있으면 값 쪽 식별자는 바뀌는데 타입 쪽 참조는 원래
11
- * 이름 그대로 남아 출력물이 타입체크 불가능한 상태가 된다
12
- * (`typeof(runtimeValue)`가 obfuscate 이후에도 원본 이름을 참조하는 것이 그 예).
13
- *
14
- * 그래서 타입 정보 자체를 파이프라인 시작 시점에 전부 지워서 이 클래스의
15
- * 버그를 원천 차단한다:
16
- * - TypeAliasStatement / ExportTypeAliasStatement 문 자체를 블록에서 제거
17
- * - TypedIdentifier / FunctionParameter 의 typeAnnotation 제거
18
- * - FunctionBody 의 generics / varargTypeAnnotation / returnType 제거
19
- * - `expr :: T` (TypeAssertionExpression) 은 T를 버리고 expr로 치환
20
- */
21
- export function runStripTypes(program: Program): void {
22
- stripBlock(program.body)
23
- }
24
-
25
- function stripBlock(block: Block): void {
26
- const kept: Statement[] = []
27
- for (const stmt of block.statements) {
28
- if (stmt.type === "TypeAliasStatement" || stmt.type === "ExportTypeAliasStatement") {
29
- continue
30
- }
31
- stripStatement(stmt)
32
- kept.push(stmt)
33
- }
34
- block.statements = kept
35
- }
36
-
37
- function stripFunctionBody(func: FunctionBody): void {
38
- func.generics = []
39
- for (const param of func.params) delete param.typeAnnotation
40
- delete func.varargTypeAnnotation
41
- delete func.returnType
42
- stripBlock(func.body)
43
- }
44
-
45
- function stripStatement(stmt: Statement): void {
46
- switch (stmt.type) {
47
- case "LocalStatement":
48
- for (const name of stmt.names) delete name.typeAnnotation
49
- for (let i = 0; i < stmt.init.length; i++) stmt.init[i] = stripExpr(stmt.init[i])
50
- return
51
-
52
- case "LocalFunctionStatement":
53
- stripFunctionBody(stmt.func)
54
- return
55
-
56
- case "FunctionDeclarationStatement":
57
- stripFunctionBody(stmt.func)
58
- return
59
-
60
- case "AssignmentStatement":
61
- for (let i = 0; i < stmt.targets.length; i++) stmt.targets[i] = stripExpr(stmt.targets[i])
62
- for (let i = 0; i < stmt.values.length; i++) stmt.values[i] = stripExpr(stmt.values[i])
63
- return
64
-
65
- case "CompoundAssignmentStatement":
66
- stmt.target = stripExpr(stmt.target)
67
- stmt.value = stripExpr(stmt.value)
68
- return
69
-
70
- case "CallStatement":
71
- stmt.expression = stripExpr(stmt.expression) as typeof stmt.expression
72
- return
73
-
74
- case "DoStatement":
75
- stripBlock(stmt.body)
76
- return
77
-
78
- case "WhileStatement":
79
- stmt.condition = stripExpr(stmt.condition)
80
- stripBlock(stmt.body)
81
- return
82
-
83
- case "RepeatStatement":
84
- stripBlock(stmt.body)
85
- stmt.condition = stripExpr(stmt.condition)
86
- return
87
-
88
- case "IfStatement":
89
- for (const clause of stmt.clauses) {
90
- clause.condition = stripExpr(clause.condition)
91
- stripBlock(clause.body)
92
- }
93
- if (stmt.alternate) stripBlock(stmt.alternate)
94
- return
95
-
96
- case "NumericForStatement":
97
- delete stmt.variable.typeAnnotation
98
- stmt.start = stripExpr(stmt.start)
99
- stmt.end = stripExpr(stmt.end)
100
- if (stmt.step) stmt.step = stripExpr(stmt.step)
101
- stripBlock(stmt.body)
102
- return
103
-
104
- case "GenericForStatement":
105
- for (const v of stmt.variables) delete v.typeAnnotation
106
- for (let i = 0; i < stmt.iterators.length; i++) stmt.iterators[i] = stripExpr(stmt.iterators[i])
107
- stripBlock(stmt.body)
108
- return
109
-
110
- case "ReturnStatement":
111
- for (let i = 0; i < stmt.arguments.length; i++) stmt.arguments[i] = stripExpr(stmt.arguments[i])
112
- return
113
-
114
- case "BreakStatement":
115
- case "ContinueStatement":
116
- return
117
- }
118
- }
119
-
120
- function stripExpr(expr: Expression): Expression {
121
- switch (expr.type) {
122
- case "InterpolatedStringExpression":
123
- for (const part of expr.parts) {
124
- if (part.kind === "expression") part.expression = stripExpr(part.expression)
125
- }
126
- return expr
127
-
128
- case "FunctionExpression":
129
- stripFunctionBody(expr.func)
130
- return expr
131
-
132
- case "TableExpression":
133
- for (const field of expr.fields) {
134
- if (field.type === "TableFieldPositional") {
135
- field.value = stripExpr(field.value)
136
- } else if (field.type === "TableFieldNamed") {
137
- field.value = stripExpr(field.value)
138
- } else if (field.type === "TableFieldComputed") {
139
- field.key = stripExpr(field.key)
140
- field.value = stripExpr(field.value)
141
- }
142
- }
143
- return expr
144
-
145
- case "BinaryExpression":
146
- expr.left = stripExpr(expr.left)
147
- expr.right = stripExpr(expr.right)
148
- return expr
149
-
150
- case "UnaryExpression":
151
- expr.argument = stripExpr(expr.argument)
152
- return expr
153
-
154
- case "MemberExpression":
155
- expr.object = stripExpr(expr.object)
156
- return expr
157
-
158
- case "IndexExpression":
159
- expr.object = stripExpr(expr.object)
160
- expr.index = stripExpr(expr.index)
161
- return expr
162
-
163
- case "CallExpression":
164
- expr.callee = stripExpr(expr.callee)
165
- for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
166
- return expr
167
-
168
- case "MethodCallExpression":
169
- expr.object = stripExpr(expr.object)
170
- for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
171
- return expr
172
-
173
- case "ParenthesizedExpression":
174
- expr.expression = stripExpr(expr.expression)
175
- return expr
176
-
177
- case "TypeAssertionExpression":
178
- // `expr :: T` -> T를 버리고 expr만 남김 (괄호로 감싸서 우선순위 보존)
179
- return stripExpr(expr.expression)
180
-
181
- case "IfElseExpression":
182
- for (const clause of expr.clauses) {
183
- clause.condition = stripExpr(clause.condition)
184
- clause.body = stripExpr(clause.body)
185
- }
186
- expr.alternate = stripExpr(expr.alternate)
187
- return expr
188
-
189
- default:
190
- return expr
191
- }
1
+ import type {
2
+ Program, Block, Statement, Expression, FunctionBody,
3
+ } from "luau-parser"
4
+
5
+ /**
6
+ * 파이프라인 맨 앞에서 실행되어야 하는 패스.
7
+ *
8
+ * 이후 패스들(GlobalMapping, RenameVariables 등)은 값 트리만 순회하고
9
+ * `typeof(x)`, `x :: T` 같은 타입 노드 내부의 expression은 건드리지 않는다.
10
+ * 타입을 나중까지 들고 있으면 값 쪽 식별자는 바뀌는데 타입 쪽 참조는 원래
11
+ * 이름 그대로 남아 출력물이 타입체크 불가능한 상태가 된다
12
+ * (`typeof(runtimeValue)`가 obfuscate 이후에도 원본 이름을 참조하는 것이 그 예).
13
+ *
14
+ * 그래서 타입 정보 자체를 파이프라인 시작 시점에 전부 지워서 이 클래스의
15
+ * 버그를 원천 차단한다:
16
+ * - TypeAliasStatement / ExportTypeAliasStatement 문 자체를 블록에서 제거
17
+ * - TypedIdentifier / FunctionParameter 의 typeAnnotation 제거
18
+ * - FunctionBody 의 generics / varargTypeAnnotation / returnType 제거
19
+ * - `expr :: T` (TypeAssertionExpression) 은 T를 버리고 expr로 치환
20
+ */
21
+ export function runStripTypes(program: Program): void {
22
+ stripBlock(program.body)
23
+ }
24
+
25
+ function stripBlock(block: Block): void {
26
+ const kept: Statement[] = []
27
+ for (const stmt of block.statements) {
28
+ if (stmt.type === "TypeAliasStatement" || stmt.type === "ExportTypeAliasStatement") {
29
+ continue
30
+ }
31
+ stripStatement(stmt)
32
+ kept.push(stmt)
33
+ }
34
+ block.statements = kept
35
+ }
36
+
37
+ function stripFunctionBody(func: FunctionBody): void {
38
+ func.generics = []
39
+ for (const param of func.params) delete param.typeAnnotation
40
+ delete func.varargTypeAnnotation
41
+ delete func.returnType
42
+ stripBlock(func.body)
43
+ }
44
+
45
+ function stripStatement(stmt: Statement): void {
46
+ switch (stmt.type) {
47
+ case "LocalStatement":
48
+ for (const name of stmt.names) delete name.typeAnnotation
49
+ for (let i = 0; i < stmt.init.length; i++) stmt.init[i] = stripExpr(stmt.init[i])
50
+ return
51
+
52
+ case "LocalFunctionStatement":
53
+ stripFunctionBody(stmt.func)
54
+ return
55
+
56
+ case "FunctionDeclarationStatement":
57
+ stripFunctionBody(stmt.func)
58
+ return
59
+
60
+ case "AssignmentStatement":
61
+ for (let i = 0; i < stmt.targets.length; i++) stmt.targets[i] = stripExpr(stmt.targets[i])
62
+ for (let i = 0; i < stmt.values.length; i++) stmt.values[i] = stripExpr(stmt.values[i])
63
+ return
64
+
65
+ case "CompoundAssignmentStatement":
66
+ stmt.target = stripExpr(stmt.target)
67
+ stmt.value = stripExpr(stmt.value)
68
+ return
69
+
70
+ case "CallStatement":
71
+ stmt.expression = stripExpr(stmt.expression) as typeof stmt.expression
72
+ return
73
+
74
+ case "DoStatement":
75
+ stripBlock(stmt.body)
76
+ return
77
+
78
+ case "WhileStatement":
79
+ stmt.condition = stripExpr(stmt.condition)
80
+ stripBlock(stmt.body)
81
+ return
82
+
83
+ case "RepeatStatement":
84
+ stripBlock(stmt.body)
85
+ stmt.condition = stripExpr(stmt.condition)
86
+ return
87
+
88
+ case "IfStatement":
89
+ for (const clause of stmt.clauses) {
90
+ clause.condition = stripExpr(clause.condition)
91
+ stripBlock(clause.body)
92
+ }
93
+ if (stmt.alternate) stripBlock(stmt.alternate)
94
+ return
95
+
96
+ case "NumericForStatement":
97
+ delete stmt.variable.typeAnnotation
98
+ stmt.start = stripExpr(stmt.start)
99
+ stmt.end = stripExpr(stmt.end)
100
+ if (stmt.step) stmt.step = stripExpr(stmt.step)
101
+ stripBlock(stmt.body)
102
+ return
103
+
104
+ case "GenericForStatement":
105
+ for (const v of stmt.variables) delete v.typeAnnotation
106
+ for (let i = 0; i < stmt.iterators.length; i++) stmt.iterators[i] = stripExpr(stmt.iterators[i])
107
+ stripBlock(stmt.body)
108
+ return
109
+
110
+ case "ReturnStatement":
111
+ for (let i = 0; i < stmt.arguments.length; i++) stmt.arguments[i] = stripExpr(stmt.arguments[i])
112
+ return
113
+
114
+ case "BreakStatement":
115
+ case "ContinueStatement":
116
+ return
117
+ }
118
+ }
119
+
120
+ function stripExpr(expr: Expression): Expression {
121
+ switch (expr.type) {
122
+ case "InterpolatedStringExpression":
123
+ for (const part of expr.parts) {
124
+ if (part.kind === "expression") part.expression = stripExpr(part.expression)
125
+ }
126
+ return expr
127
+
128
+ case "FunctionExpression":
129
+ stripFunctionBody(expr.func)
130
+ return expr
131
+
132
+ case "TableExpression":
133
+ for (const field of expr.fields) {
134
+ if (field.type === "TableFieldPositional") {
135
+ field.value = stripExpr(field.value)
136
+ } else if (field.type === "TableFieldNamed") {
137
+ field.value = stripExpr(field.value)
138
+ } else if (field.type === "TableFieldComputed") {
139
+ field.key = stripExpr(field.key)
140
+ field.value = stripExpr(field.value)
141
+ }
142
+ }
143
+ return expr
144
+
145
+ case "BinaryExpression":
146
+ expr.left = stripExpr(expr.left)
147
+ expr.right = stripExpr(expr.right)
148
+ return expr
149
+
150
+ case "UnaryExpression":
151
+ expr.argument = stripExpr(expr.argument)
152
+ return expr
153
+
154
+ case "MemberExpression":
155
+ expr.object = stripExpr(expr.object)
156
+ return expr
157
+
158
+ case "IndexExpression":
159
+ expr.object = stripExpr(expr.object)
160
+ expr.index = stripExpr(expr.index)
161
+ return expr
162
+
163
+ case "CallExpression":
164
+ expr.callee = stripExpr(expr.callee)
165
+ for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
166
+ return expr
167
+
168
+ case "MethodCallExpression":
169
+ expr.object = stripExpr(expr.object)
170
+ for (let i = 0; i < expr.arguments.length; i++) expr.arguments[i] = stripExpr(expr.arguments[i])
171
+ return expr
172
+
173
+ case "ParenthesizedExpression":
174
+ expr.expression = stripExpr(expr.expression)
175
+ return expr
176
+
177
+ case "TypeAssertionExpression":
178
+ // `expr :: T` -> T를 버리고 expr만 남김 (괄호로 감싸서 우선순위 보존)
179
+ return stripExpr(expr.expression)
180
+
181
+ case "IfElseExpression":
182
+ for (const clause of expr.clauses) {
183
+ clause.condition = stripExpr(clause.condition)
184
+ clause.body = stripExpr(clause.body)
185
+ }
186
+ expr.alternate = stripExpr(expr.alternate)
187
+ return expr
188
+
189
+ default:
190
+ return expr
191
+ }
192
192
  }
@@ -0,0 +1,73 @@
1
+ import type { Program, Statement } from "luau-parser"
2
+ import { luauparser } from "luau-parser"
3
+ import { VmCompiler, DEFAULT_BUILTIN_GLOBALS } from "./vmify/compiler"
4
+ import { serializeProto } from "./vmify/serialize"
5
+ import { buildVmRuntimeSource } from "./vmify/runtime"
6
+ import { createOpcodeMap } from "./vmify/opcodes"
7
+ import { generateVmNames } from "./vmify/names"
8
+ import {
9
+ localStatement, identifier, call, table, namedField, returnStatement, vararg,
10
+ } from "./nodeFactory"
11
+
12
+ export interface VmifyOptions {
13
+ /**
14
+ * VM 밖에서 값을 그대로 참조해야 하는 전역 이름 목록(GETGLOBAL/SETGLOBAL 대상).
15
+ * Luau엔 getfenv가 없어서 "임의의 전역 읽기"를 흉내낼 수 없기 때문에,
16
+ * 여기 나열된 이름들만 브리지 테이블에 실제 값으로 미리 채워 넣는다.
17
+ * 나열되지 않은 전역을 참조하면 컴파일은 되지만 런타임에 nil이 나온다 — 실사용 전
18
+ * 반드시 프로그램에서 실제로 쓰는 전역 이름을 전부 이 목록에 채워야 함(TODO: 컴파일러가
19
+ * ScopeAnalysis.globalsByName을 이용해 자동으로 목록을 뽑아주도록 개선 가능).
20
+ */
21
+ builtinGlobals?: readonly string[]
22
+ /**
23
+ * opcode 번호 셔플 + 전역/필드 이름 무작위화에 쓸 난수 생성기. 지정하지 않으면
24
+ * Math.random. 재현 가능한 빌드가 필요하면(테스트 등) 시드 고정 PRNG를 넘길 것.
25
+ */
26
+ random?: () => number
27
+ }
28
+
29
+ /**
30
+ * program 전체를 바이트코드로 컴파일하고, program.body를 다음 구조로 치환한다:
31
+ *
32
+ * local <globals> = { print = print, game = game, ... }
33
+ * <런타임 인터프리터 함수 정의 — 식별자/opcode 번호는 매 빌드 무작위>
34
+ * local <protoRoot> = { ...직렬화된 바이트코드... }
35
+ * return <execute>(<protoRoot>, {}, ...)
36
+ *
37
+ * opcode 번호(opcodes.ts:createOpcodeMap)와 전역/필드 이름(names.ts:generateVmNames)을
38
+ * 매 빌드 새로 뽑기 때문에, 컴파일된 청크와 런타임 인터프리터가 산출물마다 구조적으로
39
+ * 달라진다 — "이 VM은 opcode 6이 GETGLOBAL이다" 같은 지식이 다음 빌드에는 안 통함.
40
+ *
41
+ * 주의: 이 패스는 pipeline.ts상 반드시 "가장 먼저"(StripTypes 다음) 실행돼야 한다 —
42
+ * 컴파일이 끝나면 원본 statement/expression 노드 대부분이 바이트코드 숫자로 바뀌어
43
+ * 사라지므로, 이후 패스(RenameVariables, EncryptStrings 등)가 손댈 원본 AST가 없다.
44
+ */
45
+ export function runVmify(program: Program, options: VmifyOptions): void {
46
+ const globalNames = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS
47
+ const random = options.random ?? Math.random
48
+
49
+ const opcodeMap = createOpcodeMap(random)
50
+ const names = generateVmNames(random)
51
+
52
+ const compiler = new VmCompiler(program, globalNames, opcodeMap)
53
+ const topProto = compiler.compile()
54
+
55
+ const globalsTable = table(
56
+ globalNames.map((name) => namedField(name, identifier(name))),
57
+ )
58
+
59
+ const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
60
+ const runtimeProgram = luauparser.parse(runtimeSource)
61
+ const runtimeStatements: Statement[] = runtimeProgram.body.statements
62
+
63
+ const protoLiteral = serializeProto(topProto, names)
64
+
65
+ const newBody: Statement[] = [
66
+ localStatement(names.globals, globalsTable),
67
+ ...runtimeStatements,
68
+ localStatement(names.protoRoot, protoLiteral),
69
+ returnStatement([call(identifier(names.execute), [identifier(names.protoRoot), table([]), vararg()])]),
70
+ ]
71
+
72
+ program.body.statements = newBody
73
+ }