luau-obfuscator 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +171 -74
- package/dist/index.d.cts +13 -6
- package/dist/index.d.ts +13 -6
- package/dist/index.js +171 -74
- package/generated/grammers.luau +3654 -5314
- package/generated/tests.luau +2196 -3349
- package/package.json +1 -1
- package/scripts/test.js +1 -3
- package/src/passes/StringsToExpressions.ts +6 -6
- package/src/passes/Vmify.ts +24 -11
- package/src/passes/vmify/compiler.ts +90 -13
- package/src/passes/vmify/opcodes.ts +4 -0
- package/src/passes/vmify/runtime.ts +80 -51
- package/src/passes/vmify/serialize.ts +19 -13
package/package.json
CHANGED
package/scripts/test.js
CHANGED
|
@@ -15,10 +15,8 @@ for(const filename of fs.readdirSync(smokedir)) {
|
|
|
15
15
|
const timeStart = performance.now()
|
|
16
16
|
const obfuscated = obfuscate(content, {
|
|
17
17
|
Minify: {active: false},
|
|
18
|
-
NumbersToExpressions: {active: false},
|
|
19
|
-
StringsToExpressions: {active: false}
|
|
20
18
|
})
|
|
21
19
|
const timeEnd = performance.now()
|
|
22
20
|
fs.writeFileSync(path.join(GENERATE_DIR, `${filename}`), obfuscated)
|
|
23
|
-
console.log(`obfuscating ${filename} took ${(timeEnd - timeStart) / 1000}s, length: ${content.length} -> ${obfuscated.length}`)
|
|
21
|
+
console.log(`obfuscating ${filename} took ${(timeEnd - timeStart) / 1000}s, length: ${content.length} -> ${obfuscated.length} (x${obfuscated.length / content.length})`)
|
|
24
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Program, Expression, Statement } from "luau-parser"
|
|
2
2
|
import { transformExpressions } from "./walk"
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
vmNumberLiteral, binary, call, member, identifier, table, positionalField, index,
|
|
5
5
|
localStatement, localFunctionStatement, functionParam, functionBody, functionExpression,
|
|
6
6
|
block, ifStatement, ifClause, returnStatement, paren, unary, stringLiteral,
|
|
7
7
|
numericForStatement, assignmentStatement,
|
|
@@ -50,7 +50,7 @@ function splitByteChunks(bytes: number[], min: number, max: number): number[][]
|
|
|
50
50
|
|
|
51
51
|
function stringCharCall(bytes: number[]): Expression {
|
|
52
52
|
// string.char(b1, b2, ...) — 원문 텍스트가 소스에 전혀 남지 않음
|
|
53
|
-
return call(member(identifier("string"), "char"), bytes.map(
|
|
53
|
+
return call(member(identifier("string"), "char"), bytes.map(vmNumberLiteral))
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
@@ -107,7 +107,7 @@ function buildShuffledTables(chunks: number[][]): { storeVar: string; orderVar:
|
|
|
107
107
|
for (let slot = 0; slot < n; slot++) {
|
|
108
108
|
order[storageOrder[slot]] = slot + 1
|
|
109
109
|
}
|
|
110
|
-
const orderFields = order.map((slot) => positionalField(
|
|
110
|
+
const orderFields = order.map((slot) => positionalField(vmNumberLiteral(slot)))
|
|
111
111
|
|
|
112
112
|
return {
|
|
113
113
|
storeVar,
|
|
@@ -140,7 +140,7 @@ function buildForLoopStringExpr(chunks: number[][]): Expression {
|
|
|
140
140
|
localStatement(accVar, stringLiteral("")),
|
|
141
141
|
numericForStatement(
|
|
142
142
|
iVar,
|
|
143
|
-
|
|
143
|
+
vmNumberLiteral(1),
|
|
144
144
|
unary("#", identifier(orderVar)),
|
|
145
145
|
block([
|
|
146
146
|
assignmentStatement(
|
|
@@ -187,7 +187,7 @@ function buildRecursiveStringExpr(chunks: number[][]): Expression {
|
|
|
187
187
|
]),
|
|
188
188
|
returnStatement([
|
|
189
189
|
call(identifier(recName), [
|
|
190
|
-
binary("+", identifier(iParam),
|
|
190
|
+
binary("+", identifier(iParam), vmNumberLiteral(1)),
|
|
191
191
|
binary(
|
|
192
192
|
"..",
|
|
193
193
|
identifier(accParam),
|
|
@@ -200,7 +200,7 @@ function buildRecursiveStringExpr(chunks: number[][]): Expression {
|
|
|
200
200
|
return iife([
|
|
201
201
|
...stmts,
|
|
202
202
|
localFunctionStatement(recName, functionBody([functionParam(iParam), functionParam(accParam)], body)),
|
|
203
|
-
returnStatement([call(identifier(recName), [
|
|
203
|
+
returnStatement([call(identifier(recName), [vmNumberLiteral(1), stringLiteral("")])]),
|
|
204
204
|
])
|
|
205
205
|
}
|
|
206
206
|
|
package/src/passes/Vmify.ts
CHANGED
|
@@ -24,12 +24,12 @@ function markRuntimeNumbersAsStructural(body: { statements: Statement[] }): void
|
|
|
24
24
|
|
|
25
25
|
export interface VmifyOptions {
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
27
|
+
* 스코프 분석에 미리 등록해둘 전역 이름 시드 목록. 여기 없는 이름이라도 프로그램에서
|
|
28
|
+
* 실제로 참조되면(로컬/파라미터/업밸류로 안 풀리는 식별자는 전부) 스코프 분석이
|
|
29
|
+
* 자동으로 전역으로 잡아내므로, 브릿지 테이블은 이 옵션이 아니라
|
|
30
|
+
* VmCompiler.getUsedGlobalNames()(실제 사용된 전역 전체)로 만든다 — 옛날엔 이 목록에
|
|
31
|
+
* 없는 이름을 쓰면 컴파일은 되는데 GETGLOBAL이 조용히 nil을 반환해서
|
|
32
|
+
* "attempt to call a nil value"로 터졌었음.
|
|
33
33
|
*/
|
|
34
34
|
builtinGlobals?: readonly string[]
|
|
35
35
|
/**
|
|
@@ -37,6 +37,13 @@ export interface VmifyOptions {
|
|
|
37
37
|
* Math.random. 재현 가능한 빌드가 필요하면(테스트 등) 시드 고정 PRNG를 넘길 것.
|
|
38
38
|
*/
|
|
39
39
|
random?: () => number
|
|
40
|
+
/**
|
|
41
|
+
* 디버그 빌드. 켜면 각 인스트럭션에 원본 소스 줄 번호를, CALL에는 호출 대상 이름을
|
|
42
|
+
* 함께 직렬화하고, 런타임 CALL 핸들러가 non-callable 값을 부르려 할 때 그냥
|
|
43
|
+
* "attempt to call a nil value"가 아니라 "[vmdebug] ... '대상이름' (source line N)"으로
|
|
44
|
+
* 원본 위치를 찍어준다. 산출물이 조금 커지고 VM 시그니처가 노출되므로 진단용으로만 쓸 것.
|
|
45
|
+
*/
|
|
46
|
+
debug?: boolean
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -56,25 +63,31 @@ export interface VmifyOptions {
|
|
|
56
63
|
* 사라지므로, 이후 패스(RenameVariables, EncryptStrings 등)가 손댈 원본 AST가 없다.
|
|
57
64
|
*/
|
|
58
65
|
export function runVmify(program: Program, options: VmifyOptions): void {
|
|
59
|
-
const
|
|
66
|
+
const builtinGlobals = options.builtinGlobals ?? DEFAULT_BUILTIN_GLOBALS
|
|
60
67
|
const random = options.random ?? Math.random
|
|
61
68
|
|
|
69
|
+
const debug = options.debug ?? false
|
|
62
70
|
const opcodeMap = createOpcodeMap(random)
|
|
63
71
|
const names = generateVmNames(random)
|
|
64
72
|
|
|
65
|
-
const compiler = new VmCompiler(program,
|
|
73
|
+
const compiler = new VmCompiler(program, builtinGlobals, opcodeMap, debug)
|
|
66
74
|
const topProto = compiler.compile()
|
|
67
75
|
|
|
76
|
+
// builtinGlobals(시드 목록)가 아니라 실제로 프로그램이 참조한 전역 전체로 브릿지를
|
|
77
|
+
// 만든다 — game/task 같은 시드 목록에 없는 Roblox API나 실행기 전역을 스크립트가
|
|
78
|
+
// 쓰더라도 스코프 분석이 이미 전역으로 잡아뒀으므로 여기서 다 커버된다.
|
|
79
|
+
const usedGlobalNames = compiler.getUsedGlobalNames()
|
|
80
|
+
|
|
68
81
|
const globalsTable = table(
|
|
69
|
-
|
|
82
|
+
usedGlobalNames.map((name) => namedField(name, identifier(name))),
|
|
70
83
|
)
|
|
71
84
|
|
|
72
|
-
const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
|
|
85
|
+
const runtimeSource = buildVmRuntimeSource(names, opcodeMap, debug)
|
|
73
86
|
const runtimeProgram = luauparser.parse(runtimeSource)
|
|
74
87
|
const runtimeStatements: Statement[] = runtimeProgram.body.statements
|
|
75
88
|
markRuntimeNumbersAsStructural(runtimeProgram.body)
|
|
76
89
|
|
|
77
|
-
const protoLiteral = serializeProto(topProto, names)
|
|
90
|
+
const protoLiteral = serializeProto(topProto, names, debug)
|
|
78
91
|
|
|
79
92
|
const newBody: Statement[] = [
|
|
80
93
|
localStatement(names.globals, globalsTable),
|
|
@@ -33,6 +33,8 @@ interface FuncState {
|
|
|
33
33
|
upvalIndexByBinding: Map<BindingId, number>
|
|
34
34
|
breakPatchStack: number[][]
|
|
35
35
|
continuePatchStack: number[][]
|
|
36
|
+
/** debug 빌드에서 emit()이 인스트럭션에 박아넣을 "현재 컴파일 중인 소스 줄". */
|
|
37
|
+
curLine?: number
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export class VmCompiler {
|
|
@@ -42,10 +44,29 @@ export class VmCompiler {
|
|
|
42
44
|
private protoIdCounter = 0
|
|
43
45
|
private declToBinding = new Map<object, BindingId>()
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* 실제로 프로그램이 참조하는 모든 전역 이름(사전 등록된 builtinGlobals +
|
|
49
|
+
* 스코프 분석이 자동으로 찾아낸, 로컬/파라미터/업밸류로 안 풀리는 나머지 식별자
|
|
50
|
+
* 전부)을 돌려준다. Vmify.ts가 이 목록으로 브릿지 테이블(globals)을 만든다.
|
|
51
|
+
*
|
|
52
|
+
* 예전엔 호출자가 넘긴 builtinGlobals 목록만 브릿지에 넣었는데, 그 목록에
|
|
53
|
+
* 없는 전역(예: Roblox API나 실행기 전용 전역을 다 못 채운 경우)을 참조하면
|
|
54
|
+
* 컴파일은 성공하지만 GETGLOBAL이 조용히 nil을 반환해서 "attempt to call a
|
|
55
|
+
* nil value"로 터진다 — 게다가 원인이 VM 디스패치 쪽 스택에서만 보여서
|
|
56
|
+
* "opcode 매핑이 깨졌나?" 하고 엉뚱한 곳을 의심하게 만든다. analyzeScopes는
|
|
57
|
+
* 어차피 로컬/업밸류로 못 푸는 식별자를 전부 global 바인딩으로 잡아두므로,
|
|
58
|
+
* builtinGlobals에 없던 이름도 여기 globalsByName에는 다 들어있다 —
|
|
59
|
+
* 하드코딩된 허용목록 대신 이걸 그대로 쓰면 커버리지 문제 자체가 사라진다.
|
|
60
|
+
*/
|
|
61
|
+
getUsedGlobalNames(): string[] {
|
|
62
|
+
return Array.from(this.analysis.globalsByName.keys())
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
constructor(
|
|
46
66
|
private program: Program,
|
|
47
67
|
builtinGlobals: readonly string[] = DEFAULT_BUILTIN_GLOBALS,
|
|
48
68
|
private opcodeMap?: OpcodeMap,
|
|
69
|
+
private debug = false,
|
|
49
70
|
) {
|
|
50
71
|
this.analysis = luauparser.analyzeScopes(program, { builtinGlobals })
|
|
51
72
|
this.enclosing = buildEnclosingFunctionMap(program)
|
|
@@ -97,10 +118,34 @@ export class VmCompiler {
|
|
|
97
118
|
// 물리 번호로 바꿔서 저장한다. 직접 VmCompiler를 쓰는 테스트/디버깅 코드가
|
|
98
119
|
// opcodeMap 없이 호출할 수도 있으니 없으면 항등(그대로) 매핑으로 폴백.
|
|
99
120
|
const physicalOp = this.opcodeMap ? this.opcodeMap[op] : op
|
|
100
|
-
state.proto.code.push({ op: physicalOp, a, b, c, comment } as Instr)
|
|
121
|
+
state.proto.code.push({ op: physicalOp, a, b, c, comment, dbgLine: this.debug ? state.curLine : undefined } as Instr)
|
|
101
122
|
return state.proto.code.length - 1
|
|
102
123
|
}
|
|
103
124
|
|
|
125
|
+
/** debug 빌드: 방금 emit한 CALL 인스트럭션에 "호출 대상 표현식 문자열"과 그 소스 줄을
|
|
126
|
+
* 박아넣는다(런타임이 non-callable을 잡았을 때 사람이 읽을 위치를 찍게). */
|
|
127
|
+
private stampCall(state: FuncState, idx: number, callExpr: Expression, calleeExpr: Expression): void {
|
|
128
|
+
if (!this.debug) return
|
|
129
|
+
const instr = state.proto.code[idx]
|
|
130
|
+
instr.dbgName = this.describeExpr(calleeExpr)
|
|
131
|
+
const line = (calleeExpr as { line?: { start: number } }).line ?? (callExpr as { line?: { start: number } }).line
|
|
132
|
+
if (line) instr.dbgLine = line.start
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 표현식을 사람이 읽을 짧은 문자열로. 위치 추적용이라 완벽할 필요는 없음. */
|
|
136
|
+
private describeExpr(e: Expression): string {
|
|
137
|
+
switch (e.type) {
|
|
138
|
+
case "Identifier": return e.name
|
|
139
|
+
case "MemberExpression": return `${this.describeExpr(e.object)}.${e.property.name}`
|
|
140
|
+
case "MethodCallExpression": return `${this.describeExpr(e.object)}:${e.method.name}(...)`
|
|
141
|
+
case "IndexExpression": return `${this.describeExpr(e.object)}[...]`
|
|
142
|
+
case "CallExpression": return `${this.describeExpr(e.callee)}(...)`
|
|
143
|
+
case "ParenthesizedExpression": return `(${this.describeExpr(e.expression)})`
|
|
144
|
+
case "StringLiteral": return `"${String(e.value).slice(0, 20)}"`
|
|
145
|
+
default: return `<${e.type}>`
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
104
149
|
private konst(state: FuncState, value: ConstValue): number {
|
|
105
150
|
return internConst(state.proto, value)
|
|
106
151
|
}
|
|
@@ -201,6 +246,7 @@ export class VmCompiler {
|
|
|
201
246
|
}
|
|
202
247
|
|
|
203
248
|
private compileStatement(stmt: Statement, state: FuncState): void {
|
|
249
|
+
if (this.debug && (stmt as { line?: { start: number } }).line) state.curLine = (stmt as { line: { start: number } }).line.start
|
|
204
250
|
switch (stmt.type) {
|
|
205
251
|
case "LocalStatement": {
|
|
206
252
|
// 순서 중요: 로컬(및 캡처된 것의 박스)을 "먼저" 선언해서 레지스터를 확정한 뒤에
|
|
@@ -235,11 +281,39 @@ export class VmCompiler {
|
|
|
235
281
|
return
|
|
236
282
|
}
|
|
237
283
|
case "AssignmentStatement": {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
284
|
+
// LocalStatement와 동일한 다중값 규칙을 따라야 한다: 우변의 "마지막" 식이
|
|
285
|
+
// Call/MethodCall/Vararg면 남는 대상 개수만큼 펼쳐야 하고(`a, b = f()` →
|
|
286
|
+
// a=1번째, b=2번째 반환값), 그 외엔 각자 1개씩. 예전 구현은 각 값을 따로
|
|
287
|
+
// compileExpr해서 대상 개수보다 값이 적으면 "마지막 값 하나를 재사용"했는데,
|
|
288
|
+
// 그 바람에 `Tools, Manager = Nova.Create()`에서 Manager가 2번째 반환값이
|
|
289
|
+
// 아니라 Tools(1번째)를 그대로 받아 "attempt to call a nil value"로 터졌다.
|
|
290
|
+
//
|
|
291
|
+
// 모든 우변을 먼저 연속 레지스터 블록(base..)에 계산한 뒤 대상에 대입한다 —
|
|
292
|
+
// 이 순서라야 `a, b = b, a`(스왑)처럼 대입 전에 우변을 전부 평가하는
|
|
293
|
+
// Lua 의미가 보존된다.
|
|
294
|
+
const targets = stmt.targets
|
|
295
|
+
const values = stmt.values
|
|
296
|
+
const base = state.regs.top()
|
|
297
|
+
if (values.length > 0) {
|
|
298
|
+
for (let i = 0; i < values.length - 1; i++) {
|
|
299
|
+
this.compileExprTo(values[i], state, base + i)
|
|
300
|
+
}
|
|
301
|
+
const lastIdx = values.length - 1
|
|
302
|
+
const remaining = Math.max(targets.length - lastIdx, 1)
|
|
303
|
+
this.compileExprMultiInto(values[lastIdx], state, base + lastIdx, remaining)
|
|
304
|
+
}
|
|
305
|
+
// 값보다 대상이 많으면 나머지 대상은 nil.
|
|
306
|
+
const produced = values.length === 0 ? 0 : (values.length - 1) + Math.max(targets.length - (values.length - 1), 1)
|
|
307
|
+
for (let i = produced; i < targets.length; i++) {
|
|
308
|
+
this.emit(state, Opcode.LOADNIL, base + i, base + i, 0)
|
|
309
|
+
}
|
|
310
|
+
// 대입: 대상 프리픽스(테이블/키) 평가용 임시가 아직 안 쓴 값 슬롯을 덮지
|
|
311
|
+
// 않도록, 매 대입 전에 freereg를 값 블록 위(base+targets.length)로 올려둔다.
|
|
312
|
+
for (let i = 0; i < targets.length; i++) {
|
|
313
|
+
state.regs.freeTemp(base + targets.length)
|
|
314
|
+
this.compileAssignTarget(targets[i], base + i, state)
|
|
315
|
+
}
|
|
316
|
+
state.regs.freeTemp(base)
|
|
243
317
|
return
|
|
244
318
|
}
|
|
245
319
|
case "CompoundAssignmentStatement":
|
|
@@ -617,7 +691,7 @@ export class VmCompiler {
|
|
|
617
691
|
// CallExpression과 동일한 이유로 top()을 미리 캐싱하지 않고 callee의 반환값을 base로 씀.
|
|
618
692
|
const base = this.compileExpr(callExpr.callee, state)
|
|
619
693
|
this.compileArgsContiguous(callExpr.arguments, state, base + 1)
|
|
620
|
-
this.emit(state, Opcode.CALL, base, callExpr.arguments.length + 1, wantCount + 1)
|
|
694
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, base, callExpr.arguments.length + 1, wantCount + 1), callExpr, callExpr.callee)
|
|
621
695
|
return base
|
|
622
696
|
}
|
|
623
697
|
|
|
@@ -676,7 +750,7 @@ export class VmCompiler {
|
|
|
676
750
|
if (expr.type === "CallExpression") {
|
|
677
751
|
const calleeReg = this.compileExpr(expr.callee, state)
|
|
678
752
|
const b = this.compileCallArgsAndGetB(expr.arguments, state, calleeReg + 1)
|
|
679
|
-
this.emit(state, Opcode.CALL, calleeReg, b, wantCount + 1)
|
|
753
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, calleeReg, b, wantCount + 1), expr, expr.callee)
|
|
680
754
|
for (let i = 0; i < wantCount; i++) {
|
|
681
755
|
if (calleeReg + i !== base + i) this.emit(state, Opcode.MOVE, base + i, calleeReg + i, 0)
|
|
682
756
|
}
|
|
@@ -691,7 +765,7 @@ export class VmCompiler {
|
|
|
691
765
|
state.regs.allocTemp()
|
|
692
766
|
const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2)
|
|
693
767
|
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
694
|
-
this.emit(state, Opcode.CALL, fnSlot, b, wantCount + 1)
|
|
768
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, fnSlot, b, wantCount + 1), expr, expr)
|
|
695
769
|
for (let i = 0; i < wantCount; i++) {
|
|
696
770
|
if (fnSlot + i !== base + i) this.emit(state, Opcode.MOVE, base + i, fnSlot + i, 0)
|
|
697
771
|
}
|
|
@@ -722,7 +796,7 @@ export class VmCompiler {
|
|
|
722
796
|
if (calleeReg !== base) this.emit(state, Opcode.MOVE, base, calleeReg, 0)
|
|
723
797
|
state.regs.freeTemp(base + 1)
|
|
724
798
|
const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1)
|
|
725
|
-
this.emit(state, Opcode.CALL, base, b, 0)
|
|
799
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, base, b, 0), expr, expr.callee)
|
|
726
800
|
return
|
|
727
801
|
}
|
|
728
802
|
if (expr.type === "MethodCallExpression") {
|
|
@@ -738,7 +812,7 @@ export class VmCompiler {
|
|
|
738
812
|
state.regs.freeTemp(base + 2)
|
|
739
813
|
const argB = this.compileCallArgsAndGetB(expr.arguments, state, base + 2)
|
|
740
814
|
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
741
|
-
this.emit(state, Opcode.CALL, base, b, 0)
|
|
815
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, base, b, 0), expr, expr)
|
|
742
816
|
return
|
|
743
817
|
}
|
|
744
818
|
if (expr.type === "VarargExpression") {
|
|
@@ -767,6 +841,7 @@ export class VmCompiler {
|
|
|
767
841
|
}
|
|
768
842
|
|
|
769
843
|
private compileExpr(expr: Expression, state: FuncState, discard = false): number {
|
|
844
|
+
if (this.debug && (expr as { line?: { start: number } }).line) state.curLine = (expr as { line: { start: number } }).line.start
|
|
770
845
|
switch (expr.type) {
|
|
771
846
|
case "NilLiteral": {
|
|
772
847
|
const r = state.regs.allocTemp()
|
|
@@ -896,7 +971,8 @@ export class VmCompiler {
|
|
|
896
971
|
// 쓴 임시 레지스터 때문에 함수 값 자체는 그 다음 슬롯에 옴).
|
|
897
972
|
const base = this.compileExpr(expr.callee, state)
|
|
898
973
|
const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1)
|
|
899
|
-
this.emit(state, Opcode.CALL, base, b, discard ? 1 : 2)
|
|
974
|
+
const callIdx = this.emit(state, Opcode.CALL, base, b, discard ? 1 : 2)
|
|
975
|
+
this.stampCall(state, callIdx, expr, expr.callee)
|
|
900
976
|
state.regs.freeTemp(base + (discard ? 0 : 1))
|
|
901
977
|
return base
|
|
902
978
|
}
|
|
@@ -910,7 +986,8 @@ export class VmCompiler {
|
|
|
910
986
|
// self가 항상 고정으로 하나 더 붙으므로: 열려있으면(0) 그대로 0(런타임이
|
|
911
987
|
// multiTop까지 다 잡아줌, self도 그 범위 안에 포함됨), 아니면 +1(self 몫).
|
|
912
988
|
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
913
|
-
this.emit(state, Opcode.CALL, fnSlot, b, discard ? 1 : 2)
|
|
989
|
+
const callIdx = this.emit(state, Opcode.CALL, fnSlot, b, discard ? 1 : 2)
|
|
990
|
+
this.stampCall(state, callIdx, expr, expr)
|
|
914
991
|
state.regs.freeTemp(fnSlot + (discard ? 0 : 1))
|
|
915
992
|
return fnSlot
|
|
916
993
|
}
|
|
@@ -50,6 +50,10 @@ export interface Instr {
|
|
|
50
50
|
c: number
|
|
51
51
|
/** JMP/EQ/LT/LE/TEST/CALL 등에서 사람이 읽을 디버깅용 주석. 런타임엔 안 쓰임. */
|
|
52
52
|
comment?: string
|
|
53
|
+
/** debug 빌드에서만: 이 인스트럭션이 나온 원본 소스(입력 .luau) 줄 번호. */
|
|
54
|
+
dbgLine?: number
|
|
55
|
+
/** debug 빌드에서만: CALL이면 호출 대상 표현식을 사람이 읽을 문자열로(예: "print", "obj:method"). */
|
|
56
|
+
dbgName?: string
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/** RK 인코딩 헬퍼: 상수는 음수(-idx-1), 레지스터는 그대로 양수. */
|