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,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,22 +53,193 @@ function stringCharCall(bytes: number[]): Expression {
35
53
  return call(member(identifier("string"), "char"), bytes.map(numberLiteral))
36
54
  }
37
55
 
56
+ /**
57
+ * 청크를 소스에 보이는 순서 그대로 ".."로 이어붙인다.
58
+ *
59
+ * 예전에는 왼쪽으로만 쌓는 선형 체인(`(((a..b)..c)..d)...`)이었는데, 이러면
60
+ * depth == chunks.length라서 큰 문자열 리터럴(수백만 바이트) 하나만 있어도
61
+ * 표현식 중첩 깊이가 수백만까지 치솟는다. 이 트랜스파일러 자체의 스택뿐 아니라
62
+ * 실제 Luau/Lua 컴파일러도 문법 중첩 레벨에 제한(대략 200선)이 있어서, 그 한계를
63
+ * 넘는 순간 우리 쪽이 아니라 Roblox/Luau 파서에서 컴파일이 실패한다.
64
+ * 그래서 균형 이진 트리로 묶어 depth를 O(log n)으로 낮춘다 — 결합 순서만 바뀔 뿐
65
+ * ".."는 결합법칙이 성립하므로 결과 문자열은 동일하다.
66
+ */
38
67
  function buildConcatChain(chunks: number[][]): Expression {
39
- let expr: Expression = stringCharCall(chunks[0])
40
- for (let i = 1; i < chunks.length; i++) {
41
- expr = binary("..", expr, stringCharCall(chunks[i]))
68
+ function build(lo: number, hi: number): Expression {
69
+ if (hi - lo === 1) return stringCharCall(chunks[lo])
70
+ const mid = lo + Math.floor((hi - lo) / 2)
71
+ return binary("..", build(lo, mid), build(mid, hi))
72
+ }
73
+ return build(0, chunks.length)
74
+ }
75
+
76
+ /** [0, n)의 무작위 순열 (Fisher-Yates) */
77
+ function shuffledIndices(n: number): number[] {
78
+ const arr = Array.from({ length: n }, (_, i) => i)
79
+ for (let i = n - 1; i > 0; i--) {
80
+ const j = randomInt(0, i)
81
+ ;[arr[i], arr[j]] = [arr[j], arr[i]]
82
+ }
83
+ return arr
84
+ }
85
+
86
+ /** (function() ... end)() — 문(statement)들을 하나의 표현식으로 감싸는 즉시실행 함수. */
87
+ function iife(statements: Statement[]): Expression {
88
+ return call(paren(functionExpression(functionBody([], block(statements)))), [])
89
+ }
90
+
91
+ /**
92
+ * 청크들을 뒤섞어 store 테이블에 저장하고, 원래 순서를 가리키는 order 테이블을 따로 둔다.
93
+ * store에 나타나는 물리적 순서 == 원문 순서가 아니므로, 소스를 눈으로 읽거나
94
+ * 텍스트 패턴만으로 concat해서는 원문이 나오지 않는다 — order를 통해 런타임에
95
+ * 역참조해야만 올바른 순서가 나온다.
96
+ */
97
+ function buildShuffledTables(chunks: number[][]): { storeVar: string; orderVar: string; stmts: Statement[] } {
98
+ const n = chunks.length
99
+ const storageOrder = shuffledIndices(n) // storageOrder[slot] = 원본 청크 인덱스
100
+ const storeVar = randomName()
101
+ const orderVar = randomName()
102
+
103
+ const storeFields = storageOrder.map((originalIdx) => positionalField(stringCharCall(chunks[originalIdx])))
104
+
105
+ // order[originalIdx] = slot(1-based) — 원본 위치 k에 해당하는 조각이 store의 몇 번째에 있는지
106
+ const order = new Array<number>(n)
107
+ for (let slot = 0; slot < n; slot++) {
108
+ order[storageOrder[slot]] = slot + 1
109
+ }
110
+ const orderFields = order.map((slot) => positionalField(numberLiteral(slot)))
111
+
112
+ return {
113
+ storeVar,
114
+ orderVar,
115
+ stmts: [
116
+ localStatement(storeVar, table(storeFields)),
117
+ localStatement(orderVar, table(orderFields)),
118
+ ],
42
119
  }
43
- return expr
44
120
  }
45
121
 
122
+ /**
123
+ * (function()
124
+ * local store = { ... 뒤섞인 순서의 string.char(...) 조각들 ... }
125
+ * local order = { ... }
126
+ * local acc = ""
127
+ * for i = 1, #order do
128
+ * acc = acc .. store[order[i]]
129
+ * end
130
+ * return acc
131
+ * end)()
132
+ */
133
+ function buildForLoopStringExpr(chunks: number[][]): Expression {
134
+ const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
135
+ const accVar = randomName()
136
+ const iVar = randomName()
137
+
138
+ return iife([
139
+ ...stmts,
140
+ localStatement(accVar, stringLiteral("")),
141
+ numericForStatement(
142
+ iVar,
143
+ numberLiteral(1),
144
+ unary("#", identifier(orderVar)),
145
+ block([
146
+ assignmentStatement(
147
+ [identifier(accVar)],
148
+ [
149
+ binary(
150
+ "..",
151
+ identifier(accVar),
152
+ index(identifier(storeVar), index(identifier(orderVar), identifier(iVar))),
153
+ ),
154
+ ],
155
+ ),
156
+ ]),
157
+ ),
158
+ returnStatement([identifier(accVar)]),
159
+ ])
160
+ }
161
+
162
+ /**
163
+ * (function()
164
+ * local store = { ... }
165
+ * local order = { ... }
166
+ * local function build(i, acc)
167
+ * if i > #order then
168
+ * return acc
169
+ * end
170
+ * return build(i + 1, acc .. store[order[i]])
171
+ * end
172
+ * return build(1, "")
173
+ * end)()
174
+ */
175
+ function buildRecursiveStringExpr(chunks: number[][]): Expression {
176
+ const { storeVar, orderVar, stmts } = buildShuffledTables(chunks)
177
+ const recName = randomName()
178
+ const iParam = randomName()
179
+ const accParam = randomName()
180
+
181
+ const body = block([
182
+ ifStatement([
183
+ ifClause(
184
+ binary(">", identifier(iParam), unary("#", identifier(orderVar))),
185
+ block([returnStatement([identifier(accParam)])]),
186
+ ),
187
+ ]),
188
+ returnStatement([
189
+ call(identifier(recName), [
190
+ binary("+", identifier(iParam), numberLiteral(1)),
191
+ binary(
192
+ "..",
193
+ identifier(accParam),
194
+ index(identifier(storeVar), index(identifier(orderVar), identifier(iParam))),
195
+ ),
196
+ ]),
197
+ ]),
198
+ ])
199
+
200
+ return iife([
201
+ ...stmts,
202
+ localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
203
+ returnStatement([call(identifier(recName), [numberLiteral(1), stringLiteral("")])]),
204
+ ])
205
+ }
206
+
207
+ type ControlFlowStrategy = "forLoop" | "recursive"
208
+
209
+ function pickControlFlowStrategy(): ControlFlowStrategy {
210
+ return Math.random() < 0.5 ? "forLoop" : "recursive"
211
+ }
212
+
213
+ // 바이트 단위로 쪼개면 청크 하나당 AST 노드(CallExpression+MemberExpression+인자들)가
214
+ // 하나씩 생긴다. 수 MB짜리 문자열 리터럴(예: 임베드된 데이터 블롭)을 5~10바이트 청크로
215
+ // 쪼개면 수십~수백만 개 노드가 나와서 이 트랜스파일러 자체가 OOM으로 죽는다.
216
+ // 그 크기의 문자열은 바이트 단위로 흩뿌려봐야 어차피 노출 표면이 커서 난독화 효과도
217
+ // 떨어지므로, 청크 개수가 이 상한을 넘으면 통째로 하나의 string.char(...)/원본 리터럴로
218
+ // 두고 건드리지 않는다 (EncryptStrings 등 다른 패스가 필요하면 별도로 암호화하면 됨).
219
+ const MAX_CHUNKS_PER_STRING = 20000
220
+
46
221
  export function runStringsToExpressions(program: Program, options: StringsToExpressionsOptions): void {
222
+ const controlFlowProbability = options.controlFlowProbability ?? CONTROL_FLOW_PROBABILITY_DEFAULT
223
+
47
224
  transformExpressions(program, (expr) => {
48
225
  if (expr.type !== "StringLiteral") return
49
226
  if (expr.value.length === 0) return
50
227
 
51
228
  const bytes = toUtf8Bytes(expr.value)
229
+
230
+ // 너무 큰 문자열은 청크 단위 폭발을 피하기 위해 손대지 않는다.
231
+ const estimatedChunks = Math.ceil(bytes.length / Math.max(1, options.min))
232
+ if (estimatedChunks > MAX_CHUNKS_PER_STRING) return
233
+
52
234
  const chunks = splitByteChunks(bytes, options.min, options.max)
53
235
 
236
+ // 청크가 하나뿐이면 순서를 흩뜨려도 얻는 게 없으니 그냥 원래 방식대로.
237
+ if (chunks.length > 1 && Math.random() < controlFlowProbability) {
238
+ return pickControlFlowStrategy() === "forLoop"
239
+ ? buildForLoopStringExpr(chunks)
240
+ : buildRecursiveStringExpr(chunks)
241
+ }
242
+
54
243
  return buildConcatChain(chunks)
55
244
  })
56
245
  }
@@ -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
  }