luau-obfuscator 1.0.3 → 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 +146 -67
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +146 -67
- package/generated/grammers.luau +3480 -3309
- package/generated/tests.luau +1868 -1784
- package/package.json +1 -1
- package/src/passes/Vmify.ts +11 -3
- package/src/passes/vmify/compiler.ts +72 -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/src/passes/Vmify.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -59,10 +66,11 @@ export function runVmify(program: Program, options: VmifyOptions): void {
|
|
|
59
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, builtinGlobals, opcodeMap)
|
|
73
|
+
const compiler = new VmCompiler(program, builtinGlobals, opcodeMap, debug)
|
|
66
74
|
const topProto = compiler.compile()
|
|
67
75
|
|
|
68
76
|
// builtinGlobals(시드 목록)가 아니라 실제로 프로그램이 참조한 전역 전체로 브릿지를
|
|
@@ -74,12 +82,12 @@ export function runVmify(program: Program, options: VmifyOptions): void {
|
|
|
74
82
|
usedGlobalNames.map((name) => namedField(name, identifier(name))),
|
|
75
83
|
)
|
|
76
84
|
|
|
77
|
-
const runtimeSource = buildVmRuntimeSource(names, opcodeMap)
|
|
85
|
+
const runtimeSource = buildVmRuntimeSource(names, opcodeMap, debug)
|
|
78
86
|
const runtimeProgram = luauparser.parse(runtimeSource)
|
|
79
87
|
const runtimeStatements: Statement[] = runtimeProgram.body.statements
|
|
80
88
|
markRuntimeNumbersAsStructural(runtimeProgram.body)
|
|
81
89
|
|
|
82
|
-
const protoLiteral = serializeProto(topProto, names)
|
|
90
|
+
const protoLiteral = serializeProto(topProto, names, debug)
|
|
83
91
|
|
|
84
92
|
const newBody: Statement[] = [
|
|
85
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 {
|
|
@@ -64,6 +66,7 @@ export class VmCompiler {
|
|
|
64
66
|
private program: Program,
|
|
65
67
|
builtinGlobals: readonly string[] = DEFAULT_BUILTIN_GLOBALS,
|
|
66
68
|
private opcodeMap?: OpcodeMap,
|
|
69
|
+
private debug = false,
|
|
67
70
|
) {
|
|
68
71
|
this.analysis = luauparser.analyzeScopes(program, { builtinGlobals })
|
|
69
72
|
this.enclosing = buildEnclosingFunctionMap(program)
|
|
@@ -115,10 +118,34 @@ export class VmCompiler {
|
|
|
115
118
|
// 물리 번호로 바꿔서 저장한다. 직접 VmCompiler를 쓰는 테스트/디버깅 코드가
|
|
116
119
|
// opcodeMap 없이 호출할 수도 있으니 없으면 항등(그대로) 매핑으로 폴백.
|
|
117
120
|
const physicalOp = this.opcodeMap ? this.opcodeMap[op] : op
|
|
118
|
-
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)
|
|
119
122
|
return state.proto.code.length - 1
|
|
120
123
|
}
|
|
121
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
|
+
|
|
122
149
|
private konst(state: FuncState, value: ConstValue): number {
|
|
123
150
|
return internConst(state.proto, value)
|
|
124
151
|
}
|
|
@@ -219,6 +246,7 @@ export class VmCompiler {
|
|
|
219
246
|
}
|
|
220
247
|
|
|
221
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
|
|
222
250
|
switch (stmt.type) {
|
|
223
251
|
case "LocalStatement": {
|
|
224
252
|
// 순서 중요: 로컬(및 캡처된 것의 박스)을 "먼저" 선언해서 레지스터를 확정한 뒤에
|
|
@@ -253,11 +281,39 @@ export class VmCompiler {
|
|
|
253
281
|
return
|
|
254
282
|
}
|
|
255
283
|
case "AssignmentStatement": {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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)
|
|
261
317
|
return
|
|
262
318
|
}
|
|
263
319
|
case "CompoundAssignmentStatement":
|
|
@@ -635,7 +691,7 @@ export class VmCompiler {
|
|
|
635
691
|
// CallExpression과 동일한 이유로 top()을 미리 캐싱하지 않고 callee의 반환값을 base로 씀.
|
|
636
692
|
const base = this.compileExpr(callExpr.callee, state)
|
|
637
693
|
this.compileArgsContiguous(callExpr.arguments, state, base + 1)
|
|
638
|
-
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)
|
|
639
695
|
return base
|
|
640
696
|
}
|
|
641
697
|
|
|
@@ -694,7 +750,7 @@ export class VmCompiler {
|
|
|
694
750
|
if (expr.type === "CallExpression") {
|
|
695
751
|
const calleeReg = this.compileExpr(expr.callee, state)
|
|
696
752
|
const b = this.compileCallArgsAndGetB(expr.arguments, state, calleeReg + 1)
|
|
697
|
-
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)
|
|
698
754
|
for (let i = 0; i < wantCount; i++) {
|
|
699
755
|
if (calleeReg + i !== base + i) this.emit(state, Opcode.MOVE, base + i, calleeReg + i, 0)
|
|
700
756
|
}
|
|
@@ -709,7 +765,7 @@ export class VmCompiler {
|
|
|
709
765
|
state.regs.allocTemp()
|
|
710
766
|
const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2)
|
|
711
767
|
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
712
|
-
this.emit(state, Opcode.CALL, fnSlot, b, wantCount + 1)
|
|
768
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, fnSlot, b, wantCount + 1), expr, expr)
|
|
713
769
|
for (let i = 0; i < wantCount; i++) {
|
|
714
770
|
if (fnSlot + i !== base + i) this.emit(state, Opcode.MOVE, base + i, fnSlot + i, 0)
|
|
715
771
|
}
|
|
@@ -740,7 +796,7 @@ export class VmCompiler {
|
|
|
740
796
|
if (calleeReg !== base) this.emit(state, Opcode.MOVE, base, calleeReg, 0)
|
|
741
797
|
state.regs.freeTemp(base + 1)
|
|
742
798
|
const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1)
|
|
743
|
-
this.emit(state, Opcode.CALL, base, b, 0)
|
|
799
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, base, b, 0), expr, expr.callee)
|
|
744
800
|
return
|
|
745
801
|
}
|
|
746
802
|
if (expr.type === "MethodCallExpression") {
|
|
@@ -756,7 +812,7 @@ export class VmCompiler {
|
|
|
756
812
|
state.regs.freeTemp(base + 2)
|
|
757
813
|
const argB = this.compileCallArgsAndGetB(expr.arguments, state, base + 2)
|
|
758
814
|
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
759
|
-
this.emit(state, Opcode.CALL, base, b, 0)
|
|
815
|
+
this.stampCall(state, this.emit(state, Opcode.CALL, base, b, 0), expr, expr)
|
|
760
816
|
return
|
|
761
817
|
}
|
|
762
818
|
if (expr.type === "VarargExpression") {
|
|
@@ -785,6 +841,7 @@ export class VmCompiler {
|
|
|
785
841
|
}
|
|
786
842
|
|
|
787
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
|
|
788
845
|
switch (expr.type) {
|
|
789
846
|
case "NilLiteral": {
|
|
790
847
|
const r = state.regs.allocTemp()
|
|
@@ -914,7 +971,8 @@ export class VmCompiler {
|
|
|
914
971
|
// 쓴 임시 레지스터 때문에 함수 값 자체는 그 다음 슬롯에 옴).
|
|
915
972
|
const base = this.compileExpr(expr.callee, state)
|
|
916
973
|
const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1)
|
|
917
|
-
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)
|
|
918
976
|
state.regs.freeTemp(base + (discard ? 0 : 1))
|
|
919
977
|
return base
|
|
920
978
|
}
|
|
@@ -928,7 +986,8 @@ export class VmCompiler {
|
|
|
928
986
|
// self가 항상 고정으로 하나 더 붙으므로: 열려있으면(0) 그대로 0(런타임이
|
|
929
987
|
// multiTop까지 다 잡아줌, self도 그 범위 안에 포함됨), 아니면 +1(self 몫).
|
|
930
988
|
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
931
|
-
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)
|
|
932
991
|
state.regs.freeTemp(fnSlot + (discard ? 0 : 1))
|
|
933
992
|
return fnSlot
|
|
934
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), 레지스터는 그대로 양수. */
|
|
@@ -2,17 +2,24 @@ import { Opcode, type OpcodeMap } from "./opcodes"
|
|
|
2
2
|
import type { VmNames } from "./names"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* VM 런타임
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 소스를 새로 찍어낸다.
|
|
5
|
+
* VM 런타임 v4 — opcode마다 독립된 핸들러 함수를 만드는 구조는 v3와 동일하다.
|
|
6
|
+
* 모든 전역 식별자(__vm_execute, __vm_handlers, ...)와 frame/Instr/Proto의 테이블
|
|
7
|
+
* 필드 키가 고정 문자열이 아니라, 빌드마다 무작위로 뽑힌 names(names.ts)와 opcode 번호
|
|
8
|
+
* 재배치 opcodeMap(opcodes.ts)을 받아 그때그때 소스를 새로 찍어낸다.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
* "
|
|
10
|
+
* v3와의 결정적 차이 — dispatch를 tail call 재귀가 아니라 while 루프로 돌린다:
|
|
11
|
+
* - v3는 "Luau가 proper tail call을 보장하므로 return dispatch(frame, pc+1)가
|
|
12
|
+
* 스택을 안 쌓는다"는 전제로 각 핸들러가 dispatch를 꼬리 호출로 이어붙였다.
|
|
13
|
+
* 그런데 Luau는 (Lua 5.x와 달리) proper tail call을 하지 않는다. 정확한 스택
|
|
14
|
+
* 트레이스/디버깅을 위해 의도적으로 뺐다. 그래서 명령어 하나 실행할 때마다
|
|
15
|
+
* dispatch 프레임이 실제로 하나씩 쌓였고, 명령어 수백 개짜리 함수만 돌려도
|
|
16
|
+
* stack overflow가 났다.
|
|
17
|
+
* - v4에서는 dispatch가 while 루프를 돌며 핸들러를 호출한다. 핸들러는 dispatch를
|
|
18
|
+
* 부르지 않고 "다음 pc(숫자)"를 반환하고, RETURN만 결과 패킷(table.pack 결과)을
|
|
19
|
+
* 반환한다. 루프는 반환값이 숫자면 pc를 갱신하고, 테이블이면 그걸 풀어서 반환한다.
|
|
20
|
+
* 이제 함수 하나를 끝까지 도는 동안 스택 깊이는 일정하다.
|
|
14
21
|
* - "거대한 while+if-chain" 자체가 디컴파일러가 제일 먼저 찾는 VM 시그니처인데,
|
|
15
|
-
* 이
|
|
22
|
+
* 루프 본문이 if-chain이 아니라 handlers[op](...) 단일 디스패치라 그 패턴은 아니다.
|
|
16
23
|
* - opcode 번호 + 식별자 이름을 빌드마다 다르게 뽑기 때문에, 한 번 리버싱해서
|
|
17
24
|
* "MOVE=0, __vm_execute라는 함수가 진입점" 같은 지식을 얻어도 다음 산출물에는
|
|
18
25
|
* 그대로 안 먹힌다 — 매번 새로 분석해야 함.
|
|
@@ -21,16 +28,28 @@ import type { VmNames } from "./names"
|
|
|
21
28
|
* (compiler.ts가 emit() 시점에 같은 opcodeMap으로 인스트럭션에 번호를 박아 넣으므로
|
|
22
29
|
* 반드시 컴파일과 런타임 생성에 동일한 opcodeMap 인스턴스를 넘겨야 함).
|
|
23
30
|
*/
|
|
24
|
-
export function buildVmRuntimeSource(names: VmNames, opcodeMap: OpcodeMap): string {
|
|
31
|
+
export function buildVmRuntimeSource(names: VmNames, opcodeMap: OpcodeMap, debug = false): string {
|
|
25
32
|
const N = names
|
|
26
33
|
const op = (o: Opcode) => opcodeMap[o]
|
|
27
34
|
|
|
28
35
|
return `
|
|
29
36
|
local ${N.handlers} = {}
|
|
30
37
|
|
|
38
|
+
-- 각 핸들러는 "다음 pc(숫자)"를 반환한다. RETURN 핸들러만 table.pack 결과(테이블)를
|
|
39
|
+
-- 반환하며, 그게 함수 실행 종료 신호다. Luau는 proper tail call을 하지 않으므로
|
|
40
|
+
-- 핸들러들이 서로 꼬리 호출로 이어지면 명령어당 스택 프레임이 쌓여 터진다 — 그래서
|
|
41
|
+
-- 여기서 while 루프로 돌려 스택 깊이를 함수 실행 내내 일정하게 유지한다.
|
|
31
42
|
local function ${N.dispatch}(${N.frame}, pc)
|
|
32
|
-
local
|
|
33
|
-
|
|
43
|
+
local ${N.handlers} = ${N.handlers}
|
|
44
|
+
while true do
|
|
45
|
+
local instr = ${N.frame}.${N.code}[pc]
|
|
46
|
+
local next = ${N.handlers}[instr[1]](${N.frame}, pc, instr)
|
|
47
|
+
if type(next) == "number" then
|
|
48
|
+
pc = next
|
|
49
|
+
else
|
|
50
|
+
return table.unpack(next, 1, next.n)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
local function ${N.rk}(${N.frame}, x)
|
|
@@ -44,67 +63,67 @@ end
|
|
|
44
63
|
-- MOVE
|
|
45
64
|
${N.handlers}[${op(Opcode.MOVE)}] = function(${N.frame}, pc, instr)
|
|
46
65
|
${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.R}[instr[3]]
|
|
47
|
-
return
|
|
66
|
+
return pc + 1
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
-- LOADK
|
|
51
70
|
${N.handlers}[${op(Opcode.LOADK)}] = function(${N.frame}, pc, instr)
|
|
52
71
|
${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.K}[instr[3] + 1]
|
|
53
|
-
return
|
|
72
|
+
return pc + 1
|
|
54
73
|
end
|
|
55
74
|
|
|
56
75
|
-- LOADBOOL
|
|
57
76
|
${N.handlers}[${op(Opcode.LOADBOOL)}] = function(${N.frame}, pc, instr)
|
|
58
77
|
${N.frame}.${N.R}[instr[2]] = (instr[3] ~= 0)
|
|
59
|
-
return
|
|
78
|
+
return pc + 1
|
|
60
79
|
end
|
|
61
80
|
|
|
62
81
|
-- LOADNIL
|
|
63
82
|
${N.handlers}[${op(Opcode.LOADNIL)}] = function(${N.frame}, pc, instr)
|
|
64
83
|
for i = instr[2], instr[3] do ${N.frame}.${N.R}[i] = nil end
|
|
65
|
-
return
|
|
84
|
+
return pc + 1
|
|
66
85
|
end
|
|
67
86
|
|
|
68
87
|
-- GETUPVAL
|
|
69
88
|
${N.handlers}[${op(Opcode.GETUPVAL)}] = function(${N.frame}, pc, instr)
|
|
70
89
|
${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.upvals}[instr[3] + 1]
|
|
71
|
-
return
|
|
90
|
+
return pc + 1
|
|
72
91
|
end
|
|
73
92
|
|
|
74
93
|
-- SETUPVAL
|
|
75
94
|
${N.handlers}[${op(Opcode.SETUPVAL)}] = function(${N.frame}, pc, instr)
|
|
76
95
|
${N.frame}.${N.upvals}[instr[3] + 1] = ${N.frame}.${N.R}[instr[2]]
|
|
77
|
-
return
|
|
96
|
+
return pc + 1
|
|
78
97
|
end
|
|
79
98
|
|
|
80
99
|
-- GETGLOBAL (b는 RK가 아니라 상수풀 순수 인덱스)
|
|
81
100
|
${N.handlers}[${op(Opcode.GETGLOBAL)}] = function(${N.frame}, pc, instr)
|
|
82
101
|
${N.frame}.${N.R}[instr[2]] = ${N.globals}[${N.frame}.${N.K}[instr[3] + 1]]
|
|
83
|
-
return
|
|
102
|
+
return pc + 1
|
|
84
103
|
end
|
|
85
104
|
|
|
86
105
|
-- SETGLOBAL
|
|
87
106
|
${N.handlers}[${op(Opcode.SETGLOBAL)}] = function(${N.frame}, pc, instr)
|
|
88
107
|
${N.globals}[${N.frame}.${N.K}[instr[3] + 1]] = ${N.frame}.${N.R}[instr[2]]
|
|
89
|
-
return
|
|
108
|
+
return pc + 1
|
|
90
109
|
end
|
|
91
110
|
|
|
92
111
|
-- GETTABLE
|
|
93
112
|
${N.handlers}[${op(Opcode.GETTABLE)}] = function(${N.frame}, pc, instr)
|
|
94
113
|
${N.frame}.${N.R}[instr[2]] = ${N.frame}.${N.R}[instr[3]][${N.rk}(${N.frame}, instr[4])]
|
|
95
|
-
return
|
|
114
|
+
return pc + 1
|
|
96
115
|
end
|
|
97
116
|
|
|
98
117
|
-- SETTABLE
|
|
99
118
|
${N.handlers}[${op(Opcode.SETTABLE)}] = function(${N.frame}, pc, instr)
|
|
100
119
|
${N.frame}.${N.R}[instr[2]][${N.rk}(${N.frame}, instr[3])] = ${N.rk}(${N.frame}, instr[4])
|
|
101
|
-
return
|
|
120
|
+
return pc + 1
|
|
102
121
|
end
|
|
103
122
|
|
|
104
123
|
-- NEWTABLE
|
|
105
124
|
${N.handlers}[${op(Opcode.NEWTABLE)}] = function(${N.frame}, pc, instr)
|
|
106
125
|
${N.frame}.${N.R}[instr[2]] = {}
|
|
107
|
-
return
|
|
126
|
+
return pc + 1
|
|
108
127
|
end
|
|
109
128
|
|
|
110
129
|
-- SELF (R[a] = R[b][key]; R[a+1] = R[b])
|
|
@@ -112,90 +131,90 @@ ${N.handlers}[${op(Opcode.SELF)}] = function(${N.frame}, pc, instr)
|
|
|
112
131
|
local obj = ${N.frame}.${N.R}[instr[3]]
|
|
113
132
|
${N.frame}.${N.R}[instr[2]] = obj[${N.rk}(${N.frame}, instr[4])]
|
|
114
133
|
${N.frame}.${N.R}[instr[2] + 1] = obj
|
|
115
|
-
return
|
|
134
|
+
return pc + 1
|
|
116
135
|
end
|
|
117
136
|
|
|
118
137
|
-- ADD SUB MUL DIV MOD POW
|
|
119
138
|
${N.handlers}[${op(Opcode.ADD)}] = function(${N.frame}, pc, instr)
|
|
120
139
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) + ${N.rk}(${N.frame}, instr[4])
|
|
121
|
-
return
|
|
140
|
+
return pc + 1
|
|
122
141
|
end
|
|
123
142
|
${N.handlers}[${op(Opcode.SUB)}] = function(${N.frame}, pc, instr)
|
|
124
143
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) - ${N.rk}(${N.frame}, instr[4])
|
|
125
|
-
return
|
|
144
|
+
return pc + 1
|
|
126
145
|
end
|
|
127
146
|
${N.handlers}[${op(Opcode.MUL)}] = function(${N.frame}, pc, instr)
|
|
128
147
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) * ${N.rk}(${N.frame}, instr[4])
|
|
129
|
-
return
|
|
148
|
+
return pc + 1
|
|
130
149
|
end
|
|
131
150
|
${N.handlers}[${op(Opcode.DIV)}] = function(${N.frame}, pc, instr)
|
|
132
151
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) / ${N.rk}(${N.frame}, instr[4])
|
|
133
|
-
return
|
|
152
|
+
return pc + 1
|
|
134
153
|
end
|
|
135
154
|
${N.handlers}[${op(Opcode.MOD)}] = function(${N.frame}, pc, instr)
|
|
136
155
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) % ${N.rk}(${N.frame}, instr[4])
|
|
137
|
-
return
|
|
156
|
+
return pc + 1
|
|
138
157
|
end
|
|
139
158
|
${N.handlers}[${op(Opcode.POW)}] = function(${N.frame}, pc, instr)
|
|
140
159
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) ^ ${N.rk}(${N.frame}, instr[4])
|
|
141
|
-
return
|
|
160
|
+
return pc + 1
|
|
142
161
|
end
|
|
143
162
|
|
|
144
163
|
-- IDIV (Luau 바닥 나눗셈)
|
|
145
164
|
${N.handlers}[${op(Opcode.IDIV)}] = function(${N.frame}, pc, instr)
|
|
146
165
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) // ${N.rk}(${N.frame}, instr[4])
|
|
147
|
-
return
|
|
166
|
+
return pc + 1
|
|
148
167
|
end
|
|
149
168
|
|
|
150
169
|
-- CONCAT
|
|
151
170
|
${N.handlers}[${op(Opcode.CONCAT)}] = function(${N.frame}, pc, instr)
|
|
152
171
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) .. ${N.rk}(${N.frame}, instr[4])
|
|
153
|
-
return
|
|
172
|
+
return pc + 1
|
|
154
173
|
end
|
|
155
174
|
|
|
156
175
|
-- UNM
|
|
157
176
|
${N.handlers}[${op(Opcode.UNM)}] = function(${N.frame}, pc, instr)
|
|
158
177
|
${N.frame}.${N.R}[instr[2]] = -${N.frame}.${N.R}[instr[3]]
|
|
159
|
-
return
|
|
178
|
+
return pc + 1
|
|
160
179
|
end
|
|
161
180
|
|
|
162
181
|
-- NOT
|
|
163
182
|
${N.handlers}[${op(Opcode.NOT)}] = function(${N.frame}, pc, instr)
|
|
164
183
|
${N.frame}.${N.R}[instr[2]] = not ${N.frame}.${N.R}[instr[3]]
|
|
165
|
-
return
|
|
184
|
+
return pc + 1
|
|
166
185
|
end
|
|
167
186
|
|
|
168
187
|
-- LEN
|
|
169
188
|
${N.handlers}[${op(Opcode.LEN)}] = function(${N.frame}, pc, instr)
|
|
170
189
|
${N.frame}.${N.R}[instr[2]] = #${N.frame}.${N.R}[instr[3]]
|
|
171
|
-
return
|
|
190
|
+
return pc + 1
|
|
172
191
|
end
|
|
173
192
|
|
|
174
193
|
-- JMP (pc += a)
|
|
175
194
|
${N.handlers}[${op(Opcode.JMP)}] = function(${N.frame}, pc, instr)
|
|
176
|
-
return
|
|
195
|
+
return pc + instr[2]
|
|
177
196
|
end
|
|
178
197
|
|
|
179
198
|
-- EQ LT LE (값 생성형 비교)
|
|
180
199
|
${N.handlers}[${op(Opcode.EQ)}] = function(${N.frame}, pc, instr)
|
|
181
200
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) == ${N.rk}(${N.frame}, instr[4])
|
|
182
|
-
return
|
|
201
|
+
return pc + 1
|
|
183
202
|
end
|
|
184
203
|
${N.handlers}[${op(Opcode.LT)}] = function(${N.frame}, pc, instr)
|
|
185
204
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) < ${N.rk}(${N.frame}, instr[4])
|
|
186
|
-
return
|
|
205
|
+
return pc + 1
|
|
187
206
|
end
|
|
188
207
|
${N.handlers}[${op(Opcode.LE)}] = function(${N.frame}, pc, instr)
|
|
189
208
|
${N.frame}.${N.R}[instr[2]] = ${N.rk}(${N.frame}, instr[3]) <= ${N.rk}(${N.frame}, instr[4])
|
|
190
|
-
return
|
|
209
|
+
return pc + 1
|
|
191
210
|
end
|
|
192
211
|
|
|
193
212
|
-- TEST (if (not R[a]) != (c!=0) then pc += 2 else pc += 1 — 보통 다음 명령은 JMP)
|
|
194
213
|
${N.handlers}[${op(Opcode.TEST)}] = function(${N.frame}, pc, instr)
|
|
195
214
|
if (not ${N.frame}.${N.R}[instr[2]]) ~= (instr[4] ~= 0) then
|
|
196
|
-
return
|
|
215
|
+
return pc + 2
|
|
197
216
|
end
|
|
198
|
-
return
|
|
217
|
+
return pc + 1
|
|
199
218
|
end
|
|
200
219
|
|
|
201
220
|
-- CALL
|
|
@@ -210,7 +229,13 @@ ${N.handlers}[${op(Opcode.CALL)}] = function(${N.frame}, pc, instr)
|
|
|
210
229
|
end
|
|
211
230
|
local args = table.create(nargs)
|
|
212
231
|
for i = 1, nargs do args[i] = ${N.frame}.${N.R}[a + i] end
|
|
213
|
-
|
|
232
|
+
${debug ? ` if type(fn) ~= "function" then
|
|
233
|
+
local mt = getmetatable(fn)
|
|
234
|
+
if not (mt and mt.__call) then
|
|
235
|
+
error(("[vmdebug] attempt to call a %s value: '%s' (source line %d)"):format(type(fn), tostring(instr[6]), tonumber(instr[5]) or -1), 0)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
` : ""} local rets = table.pack(fn(table.unpack(args, 1, nargs)))
|
|
214
239
|
if c == 1 then
|
|
215
240
|
-- discard
|
|
216
241
|
elseif c == 0 then
|
|
@@ -220,23 +245,27 @@ ${N.handlers}[${op(Opcode.CALL)}] = function(${N.frame}, pc, instr)
|
|
|
220
245
|
local want = c - 1
|
|
221
246
|
for i = 1, want do ${N.frame}.${N.R}[a + i - 1] = rets[i] end
|
|
222
247
|
end
|
|
223
|
-
return
|
|
248
|
+
return pc + 1
|
|
224
249
|
end
|
|
225
250
|
|
|
226
251
|
-- RETURN
|
|
252
|
+
-- 다른 핸들러는 "다음 pc(숫자)"를 반환하지만, RETURN만은 결과값을 table.pack으로 싸서
|
|
253
|
+
-- 테이블을 반환한다 — dispatch 루프는 반환값이 숫자가 아니면 이걸 함수 종료 신호로 보고
|
|
254
|
+
-- table.unpack(next, 1, next.n)으로 풀어 최종 반환한다. table.pack이 n을 채워 주므로
|
|
255
|
+
-- 중간 nil이 있어도 반환 개수가 정확히 보존된다.
|
|
227
256
|
${N.handlers}[${op(Opcode.RETURN)}] = function(${N.frame}, pc, instr)
|
|
228
257
|
local a, b = instr[2], instr[3]
|
|
229
258
|
if b == 0 then
|
|
230
|
-
return table.unpack(${N.frame}.${N.R}, a, (${N.frame}.${N.multiTop} or (a + 1)) - 1)
|
|
259
|
+
return table.pack(table.unpack(${N.frame}.${N.R}, a, (${N.frame}.${N.multiTop} or (a + 1)) - 1))
|
|
231
260
|
end
|
|
232
|
-
return table.unpack(${N.frame}.${N.R}, a, a + b - 2)
|
|
261
|
+
return table.pack(table.unpack(${N.frame}.${N.R}, a, a + b - 2))
|
|
233
262
|
end
|
|
234
263
|
|
|
235
264
|
-- FORPREP
|
|
236
265
|
${N.handlers}[${op(Opcode.FORPREP)}] = function(${N.frame}, pc, instr)
|
|
237
266
|
local a = instr[2]
|
|
238
267
|
${N.frame}.${N.R}[a] = ${N.frame}.${N.R}[a] - ${N.frame}.${N.R}[a + 2]
|
|
239
|
-
return
|
|
268
|
+
return pc + instr[3]
|
|
240
269
|
end
|
|
241
270
|
|
|
242
271
|
-- FORLOOP
|
|
@@ -252,9 +281,9 @@ ${N.handlers}[${op(Opcode.FORLOOP)}] = function(${N.frame}, pc, instr)
|
|
|
252
281
|
end
|
|
253
282
|
if ok then
|
|
254
283
|
${N.frame}.${N.R}[a + 3] = ${N.frame}.${N.R}[a]
|
|
255
|
-
return
|
|
284
|
+
return pc + instr[3]
|
|
256
285
|
end
|
|
257
|
-
return
|
|
286
|
+
return pc + 1
|
|
258
287
|
end
|
|
259
288
|
|
|
260
289
|
-- CLOSURE
|
|
@@ -274,7 +303,7 @@ ${N.handlers}[${op(Opcode.CLOSURE)}] = function(${N.frame}, pc, instr)
|
|
|
274
303
|
${N.frame}.${N.R}[instr[2]] = function(...)
|
|
275
304
|
return ${N.execute}(childProto, capturedUpvals, ...)
|
|
276
305
|
end
|
|
277
|
-
return
|
|
306
|
+
return pc + 1 + n
|
|
278
307
|
end
|
|
279
308
|
|
|
280
309
|
-- VARARG
|
|
@@ -286,7 +315,7 @@ ${N.handlers}[${op(Opcode.VARARG)}] = function(${N.frame}, pc, instr)
|
|
|
286
315
|
if instr[3] == 0 then
|
|
287
316
|
${N.frame}.${N.multiTop} = instr[2] + count
|
|
288
317
|
end
|
|
289
|
-
return
|
|
318
|
+
return pc + 1
|
|
290
319
|
end
|
|
291
320
|
|
|
292
321
|
-- SETLIST
|
|
@@ -299,7 +328,7 @@ ${N.handlers}[${op(Opcode.SETLIST)}] = function(${N.frame}, pc, instr)
|
|
|
299
328
|
for i = 0, n - 1 do
|
|
300
329
|
tbl[startIdx + i] = ${N.frame}.${N.R}[valuesBase + i]
|
|
301
330
|
end
|
|
302
|
-
return
|
|
331
|
+
return pc + 1
|
|
303
332
|
end
|
|
304
333
|
|
|
305
334
|
function ${N.execute}(proto, upvals, ...)
|
|
@@ -26,16 +26,22 @@ function serializeConstValue(v: ConstValue): Expression {
|
|
|
26
26
|
* 테이블을 바로 찾아낼 수 있기 때문. runtime.ts가 같은 names로 instr.<key>를 읽으므로
|
|
27
27
|
* 반드시 컴파일 시점에 쓴 것과 동일한 VmNames 인스턴스를 넘겨야 한다.
|
|
28
28
|
*/
|
|
29
|
-
function serializeInstrs(proto: Proto): Expression {
|
|
30
|
-
const fields: TableField[] = proto.code.map((instr) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
function serializeInstrs(proto: Proto, debug: boolean): Expression {
|
|
30
|
+
const fields: TableField[] = proto.code.map((instr) => {
|
|
31
|
+
const tuple = [
|
|
32
|
+
positionalField(vmNumberLiteral(instr.op as number)),
|
|
33
|
+
positionalField(vmNumberLiteral(instr.a)),
|
|
34
|
+
positionalField(vmNumberLiteral(instr.b)),
|
|
35
|
+
positionalField(vmNumberLiteral(instr.c)),
|
|
36
|
+
]
|
|
37
|
+
if (debug) {
|
|
38
|
+
// [5] = 소스 줄(없으면 0), [6] = 호출 대상 이름(없으면 ""). 런타임이 non-callable을
|
|
39
|
+
// 잡았을 때 이 두 값으로 원본 위치를 찍는다. instr[1..4]는 그대로라 안 건드림.
|
|
40
|
+
tuple.push(positionalField(numberLiteral(instr.dbgLine ?? 0)))
|
|
41
|
+
tuple.push(positionalField(stringLiteral(instr.dbgName ?? "")))
|
|
42
|
+
}
|
|
43
|
+
return positionalField(table(tuple))
|
|
44
|
+
})
|
|
39
45
|
return table(fields)
|
|
40
46
|
}
|
|
41
47
|
|
|
@@ -52,15 +58,15 @@ function serializeUpvalDescs(proto: Proto, names: VmNames): Expression {
|
|
|
52
58
|
|
|
53
59
|
/** Proto 하나를 { numParams=.., hasVarargs=.., maxRegs=.., code={...}, consts={...}, upvalDescs={...}, protos={...} }로
|
|
54
60
|
* (필드 키는 전부 names에서 온 무작위 이름). */
|
|
55
|
-
export function serializeProto(proto: Proto, names: VmNames): Expression {
|
|
61
|
+
export function serializeProto(proto: Proto, names: VmNames, debug = false): Expression {
|
|
56
62
|
const fields: TableField[] = [
|
|
57
63
|
namedField(names.numParams, vmNumberLiteral(proto.numParams)),
|
|
58
64
|
namedField(names.hasVarargs, booleanLiteral(proto.hasVarargs)),
|
|
59
65
|
namedField(names.maxRegs, vmNumberLiteral(proto.maxRegs)),
|
|
60
|
-
namedField(names.code, serializeInstrs(proto)),
|
|
66
|
+
namedField(names.code, serializeInstrs(proto, debug)),
|
|
61
67
|
namedField(names.consts, serializeConsts(proto.consts)),
|
|
62
68
|
namedField(names.upvalDescs, serializeUpvalDescs(proto, names)),
|
|
63
|
-
namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names))))),
|
|
69
|
+
namedField(names.protos, table(proto.protos.map((p) => positionalField(serializeProto(p, names, debug))))),
|
|
64
70
|
]
|
|
65
71
|
return table(fields)
|
|
66
72
|
}
|