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.
- package/.github/workflows/release.yml +56 -56
- package/dist/index.cjs +2186 -95
- package/dist/index.d.cts +34 -2
- package/dist/index.d.ts +34 -2
- package/dist/index.js +2186 -95
- package/generated/grammers.luau +13643 -0
- package/generated/tests.luau +5567 -0
- package/package.json +2 -2
- package/scripts/test.js +24 -8
- package/{scripts/example.luau → smoketest/grammers.luau} +1174 -1174
- package/smoketest/tests.luau +115 -0
- package/src/config.ts +2 -1
- package/src/passes/ConstantArray.ts +90 -89
- package/src/passes/EncryptNumbers.ts +122 -65
- package/src/passes/EncryptStrings.ts +162 -82
- package/src/passes/GlobalMapping.ts +193 -193
- package/src/passes/InsertJunk.ts +184 -184
- package/src/passes/Minify.ts +5 -5
- package/src/passes/NumbersToExpressions.ts +183 -2
- package/src/passes/RenameVariables.ts +2 -9
- package/src/passes/StringsToExpressions.ts +195 -6
- package/src/passes/StripTypes.ts +191 -191
- package/src/passes/Vmify.ts +87 -0
- package/src/passes/WrapInFunction.ts +31 -31
- package/src/passes/nodeFactory.ts +36 -1
- package/src/passes/vmify/chunk.ts +54 -0
- package/src/passes/vmify/compiler.ts +1026 -0
- package/src/passes/vmify/names.ts +72 -0
- package/src/passes/vmify/opcodes.ts +80 -0
- package/src/passes/vmify/registers.ts +63 -0
- package/src/passes/vmify/runtime.ts +334 -0
- package/src/passes/vmify/scope-walk.ts +175 -0
- package/src/passes/vmify/serialize.ts +68 -0
- package/src/passes/walk.ts +141 -43
- package/src/pipeline.ts +5 -6
- package/tsconfig.json +20 -20
- package/tsup.config.ts +9 -9
- package/generated/final.luau +0 -1029
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Program, Block, Statement, Expression, FunctionBody, Binding, BindingId, ScopeAnalysis,
|
|
3
|
+
CallExpression,
|
|
4
|
+
} from "luau-parser"
|
|
5
|
+
import { luauparser } from "luau-parser"
|
|
6
|
+
import { Opcode, RK, type Instr, type OpcodeMap } from "./opcodes"
|
|
7
|
+
import { type Proto, type ConstValue, createProto, internConst } from "./chunk"
|
|
8
|
+
import { RegisterAllocator } from "./registers"
|
|
9
|
+
import { buildEnclosingFunctionMap, ownerFunctionOf, type EnclosingFunctionMap, type FuncMarker } from "./scope-walk"
|
|
10
|
+
|
|
11
|
+
/** 박스({v = value}) 안의 값에 접근할 때 쓰는 고정 키. 상수 풀에 인턴됨. */
|
|
12
|
+
const BOX_FIELD = "v"
|
|
13
|
+
|
|
14
|
+
/** Roblox/Luau 표준 전역 중 최소한. 실제 사용 시 프로젝트에 맞게 채워야 함. */
|
|
15
|
+
export const DEFAULT_BUILTIN_GLOBALS = [
|
|
16
|
+
"game", "script", "workspace", "print", "warn", "error", "pairs", "ipairs",
|
|
17
|
+
"pcall", "xpcall", "type", "typeof", "tostring", "tonumber", "select", "unpack",
|
|
18
|
+
"table", "string", "math", "os", "task", "Instance", "Vector3", "CFrame", "Color3",
|
|
19
|
+
"require", "setmetatable", "getmetatable", "rawget", "rawset", "rawequal", "next",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
interface LocalSlot {
|
|
23
|
+
reg: number
|
|
24
|
+
boxed: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FuncState {
|
|
28
|
+
proto: Proto
|
|
29
|
+
parent?: FuncState
|
|
30
|
+
marker: FuncMarker
|
|
31
|
+
regs: RegisterAllocator
|
|
32
|
+
localSlots: Map<BindingId, LocalSlot>
|
|
33
|
+
upvalIndexByBinding: Map<BindingId, number>
|
|
34
|
+
breakPatchStack: number[][]
|
|
35
|
+
continuePatchStack: number[][]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class VmCompiler {
|
|
39
|
+
private analysis: ScopeAnalysis
|
|
40
|
+
private enclosing: EnclosingFunctionMap
|
|
41
|
+
private captured = new Set<BindingId>()
|
|
42
|
+
private protoIdCounter = 0
|
|
43
|
+
private declToBinding = new Map<object, BindingId>()
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
private program: Program,
|
|
47
|
+
builtinGlobals: readonly string[] = DEFAULT_BUILTIN_GLOBALS,
|
|
48
|
+
private opcodeMap?: OpcodeMap,
|
|
49
|
+
) {
|
|
50
|
+
this.analysis = luauparser.analyzeScopes(program, { builtinGlobals })
|
|
51
|
+
this.enclosing = buildEnclosingFunctionMap(program)
|
|
52
|
+
for (const [id, binding] of this.analysis.bindings) {
|
|
53
|
+
if (binding.declarationNode) this.declToBinding.set(binding.declarationNode, id)
|
|
54
|
+
}
|
|
55
|
+
this.computeCaptured()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private computeCaptured(): void {
|
|
59
|
+
for (const binding of this.analysis.bindings.values()) {
|
|
60
|
+
if (binding.kind === "global") continue
|
|
61
|
+
if (!binding.declarationNode) continue
|
|
62
|
+
const ownerFn = ownerFunctionOf(this.enclosing, binding.declarationNode)
|
|
63
|
+
for (const ref of binding.references) {
|
|
64
|
+
const refFn = ownerFunctionOf(this.enclosing, ref)
|
|
65
|
+
if (refFn !== ownerFn) {
|
|
66
|
+
this.captured.add(binding.id)
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
compile(): Proto {
|
|
74
|
+
const top = this.newFuncState(undefined, this.enclosing.topMarker)
|
|
75
|
+
top.proto.hasVarargs = true
|
|
76
|
+
this.compileBlock(this.program.body, top)
|
|
77
|
+
this.emit(top, Opcode.RETURN, 0, 1, 0)
|
|
78
|
+
top.proto.maxRegs = top.regs.maxUsed
|
|
79
|
+
return top.proto
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private newFuncState(parent: FuncState | undefined, marker: FuncMarker): FuncState {
|
|
83
|
+
return {
|
|
84
|
+
proto: createProto(this.protoIdCounter++),
|
|
85
|
+
parent,
|
|
86
|
+
marker,
|
|
87
|
+
regs: new RegisterAllocator(),
|
|
88
|
+
localSlots: new Map(),
|
|
89
|
+
upvalIndexByBinding: new Map(),
|
|
90
|
+
breakPatchStack: [],
|
|
91
|
+
continuePatchStack: [],
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private emit(state: FuncState, op: Opcode, a: number, b: number, c: number, comment?: string): number {
|
|
96
|
+
// opcodeMap이 주어지면(runVmify 경유 실행) 논리 opcode 번호를 이 빌드의 무작위
|
|
97
|
+
// 물리 번호로 바꿔서 저장한다. 직접 VmCompiler를 쓰는 테스트/디버깅 코드가
|
|
98
|
+
// opcodeMap 없이 호출할 수도 있으니 없으면 항등(그대로) 매핑으로 폴백.
|
|
99
|
+
const physicalOp = this.opcodeMap ? this.opcodeMap[op] : op
|
|
100
|
+
state.proto.code.push({ op: physicalOp, a, b, c, comment } as Instr)
|
|
101
|
+
return state.proto.code.length - 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private konst(state: FuncState, value: ConstValue): number {
|
|
105
|
+
return internConst(state.proto, value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private bindingIdOf(node: object): BindingId | undefined {
|
|
109
|
+
return (this.analysis.bindingOf as Map<object, BindingId>).get(node)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getOrCreateUpval(state: FuncState, bindingId: BindingId): number {
|
|
113
|
+
const existing = state.upvalIndexByBinding.get(bindingId)
|
|
114
|
+
if (existing !== undefined) return existing
|
|
115
|
+
const parent = state.parent
|
|
116
|
+
if (!parent) throw new Error("vmify: binding not found in any enclosing function (compiler bug)")
|
|
117
|
+
const parentLocal = parent.localSlots.get(bindingId)
|
|
118
|
+
const idx = state.proto.upvalDescs.length
|
|
119
|
+
if (parentLocal) {
|
|
120
|
+
state.proto.upvalDescs.push({ kind: "local", index: parentLocal.reg })
|
|
121
|
+
} else {
|
|
122
|
+
const parentUpvalIdx = this.getOrCreateUpval(parent, bindingId)
|
|
123
|
+
state.proto.upvalDescs.push({ kind: "upval", index: parentUpvalIdx })
|
|
124
|
+
}
|
|
125
|
+
state.upvalIndexByBinding.set(bindingId, idx)
|
|
126
|
+
return idx
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private bindingIsLocalSomewhereUp(state: FuncState, bindingId: BindingId): boolean {
|
|
130
|
+
let s: FuncState | undefined = state
|
|
131
|
+
while (s) {
|
|
132
|
+
if (s.localSlots.has(bindingId)) return true
|
|
133
|
+
s = s.parent
|
|
134
|
+
}
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private declareLocalBinding(state: FuncState, bindingId: BindingId): number {
|
|
139
|
+
const reg = state.regs.declareLocal()
|
|
140
|
+
state.localSlots.set(bindingId, { reg, boxed: this.captured.has(bindingId) })
|
|
141
|
+
if (this.captured.has(bindingId)) this.emitNewBox(state, reg)
|
|
142
|
+
return reg
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private emitNewBox(state: FuncState, reg: number): void {
|
|
146
|
+
this.emit(state, Opcode.NEWTABLE, reg, 0, 0, "box")
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private emitLoadBinding(state: FuncState, bindingId: BindingId, dest: number): void {
|
|
150
|
+
const local = state.localSlots.get(bindingId)
|
|
151
|
+
if (local) {
|
|
152
|
+
if (local.boxed) {
|
|
153
|
+
const k = this.konst(state, BOX_FIELD)
|
|
154
|
+
this.emit(state, Opcode.GETTABLE, dest, local.reg, RK(k, true))
|
|
155
|
+
} else if (dest !== local.reg) {
|
|
156
|
+
this.emit(state, Opcode.MOVE, dest, local.reg, 0)
|
|
157
|
+
}
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (this.bindingIsLocalSomewhereUp(state, bindingId)) {
|
|
161
|
+
const idx = this.getOrCreateUpval(state, bindingId)
|
|
162
|
+
this.emit(state, Opcode.GETUPVAL, dest, idx, 0)
|
|
163
|
+
const k = this.konst(state, BOX_FIELD)
|
|
164
|
+
this.emit(state, Opcode.GETTABLE, dest, dest, RK(k, true))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
const binding = this.analysis.bindings.get(bindingId) as Binding
|
|
168
|
+
const nameIdx = this.konst(state, binding.name)
|
|
169
|
+
this.emit(state, Opcode.GETGLOBAL, dest, nameIdx, 0)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private emitStoreBinding(state: FuncState, bindingId: BindingId, src: number): void {
|
|
173
|
+
const local = state.localSlots.get(bindingId)
|
|
174
|
+
if (local) {
|
|
175
|
+
if (local.boxed) {
|
|
176
|
+
const k = this.konst(state, BOX_FIELD)
|
|
177
|
+
this.emit(state, Opcode.SETTABLE, local.reg, RK(k, true), src)
|
|
178
|
+
} else if (src !== local.reg) {
|
|
179
|
+
this.emit(state, Opcode.MOVE, local.reg, src, 0)
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
if (this.bindingIsLocalSomewhereUp(state, bindingId)) {
|
|
184
|
+
const idx = this.getOrCreateUpval(state, bindingId)
|
|
185
|
+
const tmp = state.regs.allocTemp()
|
|
186
|
+
this.emit(state, Opcode.GETUPVAL, tmp, idx, 0)
|
|
187
|
+
const k = this.konst(state, BOX_FIELD)
|
|
188
|
+
this.emit(state, Opcode.SETTABLE, tmp, RK(k, true), src)
|
|
189
|
+
state.regs.freeTemp(tmp)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const binding = this.analysis.bindings.get(bindingId) as Binding
|
|
193
|
+
const nameIdx = this.konst(state, binding.name)
|
|
194
|
+
this.emit(state, Opcode.SETGLOBAL, src, nameIdx, 0)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private compileBlock(block: Block, state: FuncState): void {
|
|
198
|
+
const saved = state.regs.saveLocalCount()
|
|
199
|
+
for (const stmt of block.statements) this.compileStatement(stmt, state)
|
|
200
|
+
state.regs.releaseLocalsTo(saved)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private compileStatement(stmt: Statement, state: FuncState): void {
|
|
204
|
+
switch (stmt.type) {
|
|
205
|
+
case "LocalStatement": {
|
|
206
|
+
// 순서 중요: 로컬(및 캡처된 것의 박스)을 "먼저" 선언해서 레지스터를 확정한 뒤에
|
|
207
|
+
// 초기화식을 컴파일해야 한다. 반대로 하면(예전 버그) 초기화값을 임시 레지스터에
|
|
208
|
+
// 계산해놓고 나서 declareLocalBinding이 캡처 여부를 보고 "같은" 레지스터에
|
|
209
|
+
// NEWTABLE(박스)을 찍어버려 방금 계산한 값이 통째로 날아간다.
|
|
210
|
+
// bindingId 해석 자체는 analyzeScopes가 정적으로 끝내놓기 때문에
|
|
211
|
+
// (`local x = x`의 우변은 바깥쪽 x를 정확히 가리킴) 레지스터를 먼저 확정해도
|
|
212
|
+
// 셰도잉 문제는 생기지 않는다.
|
|
213
|
+
const bindingIds = stmt.names.map((n) => this.bindingIdOfDecl(n))
|
|
214
|
+
for (const id of bindingIds) this.declareLocalBinding(state, id)
|
|
215
|
+
const boundary = state.regs.top()
|
|
216
|
+
// 초기화식 목록: 마지막 식이 Call/MethodCall/Vararg면 남은 이름 개수만큼
|
|
217
|
+
// 확장하고(`local a, b = pair()`), 그 외엔 각자 1개씩 채우고 모자란 이름은 nil.
|
|
218
|
+
let filledUpTo = 0 // boundary 기준으로 이미 값이 채워진 슬롯 수(패딩 시작점)
|
|
219
|
+
if (stmt.init.length > 0) {
|
|
220
|
+
for (let i = 0; i < stmt.init.length - 1; i++) {
|
|
221
|
+
this.compileExprTo(stmt.init[i], state, boundary + i)
|
|
222
|
+
}
|
|
223
|
+
const lastIdx = stmt.init.length - 1
|
|
224
|
+
const remaining = Math.max(bindingIds.length - lastIdx, 1)
|
|
225
|
+
this.compileExprMultiInto(stmt.init[lastIdx], state, boundary + lastIdx, remaining)
|
|
226
|
+
filledUpTo = lastIdx + remaining // 마지막 식이 채운 슬롯까지 전부 포함
|
|
227
|
+
}
|
|
228
|
+
for (let i = filledUpTo; i < bindingIds.length; i++) {
|
|
229
|
+
this.emit(state, Opcode.LOADNIL, boundary + i, boundary + i, 0)
|
|
230
|
+
}
|
|
231
|
+
for (let i = 0; i < bindingIds.length; i++) {
|
|
232
|
+
this.emitStoreBinding(state, bindingIds[i], boundary + i)
|
|
233
|
+
}
|
|
234
|
+
state.regs.freeTemp(boundary)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
case "AssignmentStatement": {
|
|
238
|
+
const valueRegs = stmt.values.map((e) => this.compileExpr(e, state))
|
|
239
|
+
stmt.targets.forEach((target, i) => {
|
|
240
|
+
const srcReg = valueRegs[i] ?? valueRegs[valueRegs.length - 1]
|
|
241
|
+
this.compileAssignTarget(target, srcReg, state)
|
|
242
|
+
})
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
case "CompoundAssignmentStatement":
|
|
246
|
+
this.compileCompoundAssignment(stmt, state)
|
|
247
|
+
return
|
|
248
|
+
case "CallStatement":
|
|
249
|
+
this.compileExpr(stmt.expression, state, true)
|
|
250
|
+
return
|
|
251
|
+
case "IfStatement":
|
|
252
|
+
this.compileIf(stmt, state)
|
|
253
|
+
return
|
|
254
|
+
case "WhileStatement":
|
|
255
|
+
this.compileWhile(stmt, state)
|
|
256
|
+
return
|
|
257
|
+
case "RepeatStatement":
|
|
258
|
+
this.compileRepeat(stmt, state)
|
|
259
|
+
return
|
|
260
|
+
case "NumericForStatement":
|
|
261
|
+
this.compileNumericFor(stmt, state)
|
|
262
|
+
return
|
|
263
|
+
case "GenericForStatement":
|
|
264
|
+
this.compileGenericFor(stmt, state)
|
|
265
|
+
return
|
|
266
|
+
case "DoStatement":
|
|
267
|
+
this.compileBlock(stmt.body, state)
|
|
268
|
+
return
|
|
269
|
+
case "ReturnStatement": {
|
|
270
|
+
if (stmt.arguments.length === 0) {
|
|
271
|
+
this.emit(state, Opcode.RETURN, state.regs.top(), 1, 0)
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
// `return f()` / `return ...` (인자가 딱 하나뿐이고 다중값 가능) — 결과 개수를
|
|
275
|
+
// 컴파일 타임에 모르니 열어서 그대로 전파(RETURN b=0, 런타임 multiTop 기준).
|
|
276
|
+
if (stmt.arguments.length === 1 && this.isMultiValueExpr(stmt.arguments[0])) {
|
|
277
|
+
const base = state.regs.top()
|
|
278
|
+
this.compileExprOpenInto(stmt.arguments[0], state, base)
|
|
279
|
+
this.emit(state, Opcode.RETURN, base, 0, 0)
|
|
280
|
+
state.regs.freeTemp(base)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
// 여러 개 있으면 마지막 것만 열어서 전파, 나머지는 고정 1개씩.
|
|
284
|
+
const boundary = state.regs.top()
|
|
285
|
+
for (let i = 0; i < stmt.arguments.length - 1; i++) {
|
|
286
|
+
this.compileExprTo(stmt.arguments[i], state, boundary + i)
|
|
287
|
+
}
|
|
288
|
+
const lastIdx = stmt.arguments.length - 1
|
|
289
|
+
if (this.isMultiValueExpr(stmt.arguments[lastIdx])) {
|
|
290
|
+
this.compileExprOpenInto(stmt.arguments[lastIdx], state, boundary + lastIdx)
|
|
291
|
+
this.emit(state, Opcode.RETURN, boundary, 0, 0)
|
|
292
|
+
} else {
|
|
293
|
+
this.compileExprTo(stmt.arguments[lastIdx], state, boundary + lastIdx)
|
|
294
|
+
this.emit(state, Opcode.RETURN, boundary, stmt.arguments.length + 1, 0)
|
|
295
|
+
}
|
|
296
|
+
state.regs.freeTemp(boundary)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
case "LocalFunctionStatement": {
|
|
300
|
+
// 캡처되는 경우 CLOSURE를 박스 레지스터에 직접 쓰면 안 된다 — 그 순간
|
|
301
|
+
// 박스(테이블)가 함수 값으로 덮어써져서 사라진다. 임시 레지스터에 만든 뒤
|
|
302
|
+
// emitStoreBinding으로 박스 안에 넣어야 함(재귀 함수 자기 참조도 이 순서로 지원됨).
|
|
303
|
+
const bindingId = this.bindingIdOfDecl(stmt.name)
|
|
304
|
+
const reg = this.declareLocalBinding(state, bindingId)
|
|
305
|
+
const protoIdx = this.compileFunctionBody(stmt.func, state)
|
|
306
|
+
const boxed = state.localSlots.get(bindingId)!.boxed
|
|
307
|
+
if (boxed) {
|
|
308
|
+
const tmp = state.regs.allocTemp()
|
|
309
|
+
this.emitClosure(state, tmp, protoIdx)
|
|
310
|
+
this.emitStoreBinding(state, bindingId, tmp)
|
|
311
|
+
state.regs.freeTemp(tmp)
|
|
312
|
+
} else {
|
|
313
|
+
this.emitClosure(state, reg, protoIdx)
|
|
314
|
+
}
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
case "FunctionDeclarationStatement":
|
|
318
|
+
this.compileFunctionDeclaration(stmt, state)
|
|
319
|
+
return
|
|
320
|
+
case "BreakStatement": {
|
|
321
|
+
if (state.breakPatchStack.length === 0) throw new Error("vmify: break outside loop")
|
|
322
|
+
const idx = this.emit(state, Opcode.JMP, 0, 0, 0, "break")
|
|
323
|
+
state.breakPatchStack[state.breakPatchStack.length - 1].push(idx)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
case "ContinueStatement": {
|
|
327
|
+
if (state.continuePatchStack.length === 0) throw new Error("vmify: continue outside loop")
|
|
328
|
+
const idx = this.emit(state, Opcode.JMP, 0, 0, 0, "continue")
|
|
329
|
+
state.continuePatchStack[state.continuePatchStack.length - 1].push(idx)
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
case "TypeAliasStatement":
|
|
333
|
+
case "ExportTypeAliasStatement":
|
|
334
|
+
return
|
|
335
|
+
default: {
|
|
336
|
+
const _exhaustive: never = stmt
|
|
337
|
+
throw new Error(`vmify: unhandled statement ${(_exhaustive as Statement).type}`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private compileAssignTarget(target: Expression, srcReg: number, state: FuncState): void {
|
|
343
|
+
if (target.type === "Identifier") {
|
|
344
|
+
const bindingId = this.bindingIdOf(target)
|
|
345
|
+
if (bindingId === undefined) throw new Error("vmify: unresolved identifier target")
|
|
346
|
+
this.emitStoreBinding(state, bindingId, srcReg)
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
if (target.type === "MemberExpression") {
|
|
350
|
+
const objReg = this.compileExpr(target.object, state)
|
|
351
|
+
const k = this.konst(state, target.property.name)
|
|
352
|
+
this.emit(state, Opcode.SETTABLE, objReg, RK(k, true), srcReg)
|
|
353
|
+
state.regs.freeTemp(objReg)
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
if (target.type === "IndexExpression") {
|
|
357
|
+
const objReg = this.compileExpr(target.object, state)
|
|
358
|
+
const keyReg = this.compileExpr(target.index, state)
|
|
359
|
+
this.emit(state, Opcode.SETTABLE, objReg, keyReg, srcReg)
|
|
360
|
+
state.regs.freeTemp(objReg)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
throw new Error(`vmify: unsupported assignment target ${target.type}`)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private compileCompoundAssignment(
|
|
367
|
+
stmt: Extract<Statement, { type: "CompoundAssignmentStatement" }>,
|
|
368
|
+
state: FuncState,
|
|
369
|
+
): void {
|
|
370
|
+
const opMap: Partial<Record<string, Opcode>> = {
|
|
371
|
+
"+=": Opcode.ADD, "-=": Opcode.SUB, "*=": Opcode.MUL, "/=": Opcode.DIV,
|
|
372
|
+
"//=": Opcode.IDIV, "%=": Opcode.MOD, "^=": Opcode.POW, "..=": Opcode.CONCAT,
|
|
373
|
+
}
|
|
374
|
+
const op = opMap[stmt.operator]
|
|
375
|
+
if (!op) throw new Error(`vmify: unsupported compound operator ${stmt.operator}`)
|
|
376
|
+
|
|
377
|
+
const target = stmt.target
|
|
378
|
+
if (target.type === "Identifier") {
|
|
379
|
+
const bindingId = this.bindingIdOf(target)
|
|
380
|
+
if (bindingId === undefined) throw new Error("vmify: unresolved identifier target")
|
|
381
|
+
const cur = state.regs.allocTemp()
|
|
382
|
+
this.emitLoadBinding(state, bindingId, cur)
|
|
383
|
+
const rhs = this.compileExpr(stmt.value, state)
|
|
384
|
+
this.emit(state, op, cur, cur, rhs)
|
|
385
|
+
state.regs.freeTemp(rhs)
|
|
386
|
+
this.emitStoreBinding(state, bindingId, cur)
|
|
387
|
+
state.regs.freeTemp(cur)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
if (target.type === "MemberExpression") {
|
|
391
|
+
const objReg = this.compileExpr(target.object, state)
|
|
392
|
+
const k = this.konst(state, target.property.name)
|
|
393
|
+
const cur = state.regs.allocTemp()
|
|
394
|
+
this.emit(state, Opcode.GETTABLE, cur, objReg, RK(k, true))
|
|
395
|
+
const rhs = this.compileExpr(stmt.value, state)
|
|
396
|
+
this.emit(state, op, cur, cur, rhs)
|
|
397
|
+
this.emit(state, Opcode.SETTABLE, objReg, RK(k, true), cur)
|
|
398
|
+
state.regs.freeTemp(objReg)
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
if (target.type === "IndexExpression") {
|
|
402
|
+
const objReg = this.compileExpr(target.object, state)
|
|
403
|
+
const keyReg = this.compileExpr(target.index, state)
|
|
404
|
+
const cur = state.regs.allocTemp()
|
|
405
|
+
this.emit(state, Opcode.GETTABLE, cur, objReg, keyReg)
|
|
406
|
+
const rhs = this.compileExpr(stmt.value, state)
|
|
407
|
+
this.emit(state, op, cur, cur, rhs)
|
|
408
|
+
this.emit(state, Opcode.SETTABLE, objReg, keyReg, cur)
|
|
409
|
+
state.regs.freeTemp(objReg)
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
throw new Error(`vmify: unsupported compound assignment target ${target.type}`)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private compileFunctionDeclaration(
|
|
416
|
+
stmt: Extract<Statement, { type: "FunctionDeclarationStatement" }>,
|
|
417
|
+
state: FuncState,
|
|
418
|
+
): void {
|
|
419
|
+
const protoIdx = this.compileFunctionBody(stmt.func, state)
|
|
420
|
+
const fnReg = state.regs.allocTemp()
|
|
421
|
+
this.emitClosure(state, fnReg, protoIdx)
|
|
422
|
+
|
|
423
|
+
const target = stmt.target
|
|
424
|
+
if (target.path.length === 0 && !target.method) {
|
|
425
|
+
const bindingId = this.bindingIdOf(target.base)
|
|
426
|
+
if (bindingId === undefined) throw new Error("vmify: unresolved function name")
|
|
427
|
+
this.emitStoreBinding(state, bindingId, fnReg)
|
|
428
|
+
state.regs.freeTemp(fnReg)
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const baseBindingId = this.bindingIdOf(target.base)
|
|
433
|
+
if (baseBindingId === undefined) throw new Error("vmify: unresolved function name base")
|
|
434
|
+
let objReg = state.regs.allocTemp()
|
|
435
|
+
this.emitLoadBinding(state, baseBindingId, objReg)
|
|
436
|
+
|
|
437
|
+
const navigateSegments = target.method ? target.path : target.path.slice(0, -1)
|
|
438
|
+
for (const seg of navigateSegments) {
|
|
439
|
+
const k = this.konst(state, seg.name)
|
|
440
|
+
const next = state.regs.allocTemp()
|
|
441
|
+
this.emit(state, Opcode.GETTABLE, next, objReg, RK(k, true))
|
|
442
|
+
objReg = next
|
|
443
|
+
}
|
|
444
|
+
const finalKeyName = target.method ? target.method.name : target.path[target.path.length - 1].name
|
|
445
|
+
const k = this.konst(state, finalKeyName)
|
|
446
|
+
this.emit(state, Opcode.SETTABLE, objReg, RK(k, true), fnReg)
|
|
447
|
+
state.regs.freeTemp(fnReg)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private compileIf(stmt: Extract<Statement, { type: "IfStatement" }>, state: FuncState): void {
|
|
451
|
+
const endPatches: number[] = []
|
|
452
|
+
for (const clause of stmt.clauses) {
|
|
453
|
+
const condReg = this.compileExpr(clause.condition, state)
|
|
454
|
+
this.emit(state, Opcode.TEST, condReg, 0, 1) // skip next(jump-away) when truthy
|
|
455
|
+
const jmpOverBody = this.emit(state, Opcode.JMP, 0, 0, 0, "skip-then")
|
|
456
|
+
state.regs.freeTemp(condReg)
|
|
457
|
+
this.compileBlock(clause.body, state)
|
|
458
|
+
const jmpToEnd = this.emit(state, Opcode.JMP, 0, 0, 0, "then->end")
|
|
459
|
+
endPatches.push(jmpToEnd)
|
|
460
|
+
this.patchJump(state, jmpOverBody, state.proto.code.length)
|
|
461
|
+
}
|
|
462
|
+
if (stmt.alternate) this.compileBlock(stmt.alternate, state)
|
|
463
|
+
for (const p of endPatches) this.patchJump(state, p, state.proto.code.length)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private compileWhile(stmt: Extract<Statement, { type: "WhileStatement" }>, state: FuncState): void {
|
|
467
|
+
const loopStart = state.proto.code.length
|
|
468
|
+
const condReg = this.compileExpr(stmt.condition, state)
|
|
469
|
+
this.emit(state, Opcode.TEST, condReg, 0, 1) // skip next(jump-away) when truthy
|
|
470
|
+
const exitJmp = this.emit(state, Opcode.JMP, 0, 0, 0, "while-exit")
|
|
471
|
+
state.regs.freeTemp(condReg)
|
|
472
|
+
state.breakPatchStack.push([])
|
|
473
|
+
state.continuePatchStack.push([])
|
|
474
|
+
this.compileBlock(stmt.body, state)
|
|
475
|
+
this.emit(state, Opcode.JMP, loopStart - state.proto.code.length, 0, 0, "while-back")
|
|
476
|
+
const target = state.proto.code.length
|
|
477
|
+
this.patchJump(state, exitJmp, target)
|
|
478
|
+
for (const b of state.breakPatchStack.pop()!) this.patchJump(state, b, target)
|
|
479
|
+
for (const c of state.continuePatchStack.pop()!) this.patchJump(state, c, loopStart)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private compileRepeat(stmt: Extract<Statement, { type: "RepeatStatement" }>, state: FuncState): void {
|
|
483
|
+
const loopStart = state.proto.code.length
|
|
484
|
+
state.breakPatchStack.push([])
|
|
485
|
+
state.continuePatchStack.push([])
|
|
486
|
+
const saved = state.regs.saveLocalCount()
|
|
487
|
+
for (const s of stmt.body.statements) this.compileStatement(s, state)
|
|
488
|
+
const condCheckStart = state.proto.code.length
|
|
489
|
+
const condReg = this.compileExpr(stmt.condition, state)
|
|
490
|
+
this.emit(state, Opcode.TEST, condReg, 0, 1) // skip next(jump-away) when truthy
|
|
491
|
+
const jmpBack = this.emit(state, Opcode.JMP, 0, 0, 0, "repeat-back")
|
|
492
|
+
state.regs.freeTemp(condReg)
|
|
493
|
+
this.patchJump(state, jmpBack, loopStart)
|
|
494
|
+
state.regs.releaseLocalsTo(saved)
|
|
495
|
+
const endIdx = state.proto.code.length
|
|
496
|
+
for (const b of state.breakPatchStack.pop()!) this.patchJump(state, b, endIdx)
|
|
497
|
+
for (const c of state.continuePatchStack.pop()!) this.patchJump(state, c, condCheckStart)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private compileNumericFor(stmt: Extract<Statement, { type: "NumericForStatement" }>, state: FuncState): void {
|
|
501
|
+
const saved = state.regs.saveLocalCount()
|
|
502
|
+
const base = state.regs.declareLocal()
|
|
503
|
+
state.regs.declareLocal()
|
|
504
|
+
state.regs.declareLocal()
|
|
505
|
+
const cursor = state.regs.declareLocal()
|
|
506
|
+
|
|
507
|
+
const startTmp = this.compileExpr(stmt.start, state)
|
|
508
|
+
this.emit(state, Opcode.MOVE, base, startTmp, 0)
|
|
509
|
+
state.regs.freeTemp(startTmp)
|
|
510
|
+
|
|
511
|
+
const endTmp = this.compileExpr(stmt.end, state)
|
|
512
|
+
this.emit(state, Opcode.MOVE, base + 1, endTmp, 0)
|
|
513
|
+
state.regs.freeTemp(endTmp)
|
|
514
|
+
|
|
515
|
+
const stepTmp = stmt.step ? this.compileExpr(stmt.step, state) : this.loadConst(state, 1)
|
|
516
|
+
this.emit(state, Opcode.MOVE, base + 2, stepTmp, 0)
|
|
517
|
+
state.regs.freeTemp(stepTmp)
|
|
518
|
+
|
|
519
|
+
const prep = this.emit(state, Opcode.FORPREP, base, 0, 0, "forprep")
|
|
520
|
+
const loopBodyStart = state.proto.code.length
|
|
521
|
+
|
|
522
|
+
const bindingId = this.bindingIdOfDecl(stmt.variable)
|
|
523
|
+
const captured = this.captured.has(bindingId)
|
|
524
|
+
let varReg: number
|
|
525
|
+
if (captured) {
|
|
526
|
+
varReg = state.regs.declareLocal()
|
|
527
|
+
this.emitNewBox(state, varReg)
|
|
528
|
+
const k = this.konst(state, BOX_FIELD)
|
|
529
|
+
this.emit(state, Opcode.SETTABLE, varReg, RK(k, true), cursor)
|
|
530
|
+
} else {
|
|
531
|
+
varReg = cursor
|
|
532
|
+
}
|
|
533
|
+
state.localSlots.set(bindingId, { reg: varReg, boxed: captured })
|
|
534
|
+
|
|
535
|
+
state.breakPatchStack.push([])
|
|
536
|
+
state.continuePatchStack.push([])
|
|
537
|
+
this.compileBlock(stmt.body, state)
|
|
538
|
+
const loopInstr = this.emit(state, Opcode.FORLOOP, base, 0, 0, "forloop")
|
|
539
|
+
this.patchJumpField(state, prep, loopInstr, "b")
|
|
540
|
+
this.patchJumpField(state, loopInstr, loopBodyStart, "b")
|
|
541
|
+
const endIdx = state.proto.code.length
|
|
542
|
+
for (const b of state.breakPatchStack.pop()!) this.patchJump(state, b, endIdx)
|
|
543
|
+
for (const c of state.continuePatchStack.pop()!) this.patchJump(state, c, loopInstr)
|
|
544
|
+
|
|
545
|
+
state.regs.releaseLocalsTo(saved)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private compileGenericFor(stmt: Extract<Statement, { type: "GenericForStatement" }>, state: FuncState): void {
|
|
549
|
+
const saved = state.regs.saveLocalCount()
|
|
550
|
+
const base = state.regs.declareLocal()
|
|
551
|
+
state.regs.declareLocal()
|
|
552
|
+
state.regs.declareLocal()
|
|
553
|
+
|
|
554
|
+
const iterators = stmt.iterators
|
|
555
|
+
if (iterators.length === 1 && iterators[0].type === "CallExpression") {
|
|
556
|
+
const firstReg = this.compileCallMultiInto(iterators[0] as CallExpression, state, 3)
|
|
557
|
+
for (let i = 0; i < 3; i++) this.emit(state, Opcode.MOVE, base + i, firstReg + i, 0)
|
|
558
|
+
state.regs.freeTemp(firstReg)
|
|
559
|
+
} else {
|
|
560
|
+
for (let i = 0; i < 3; i++) {
|
|
561
|
+
if (i < iterators.length) {
|
|
562
|
+
const r = this.compileExpr(iterators[i], state)
|
|
563
|
+
this.emit(state, Opcode.MOVE, base + i, r, 0)
|
|
564
|
+
state.regs.freeTemp(r)
|
|
565
|
+
} else {
|
|
566
|
+
this.emit(state, Opcode.LOADNIL, base + i, base + i, 0)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const loopStart = state.proto.code.length
|
|
572
|
+
const nVars = stmt.variables.length
|
|
573
|
+
const callBase = state.regs.top()
|
|
574
|
+
const fnSlot = state.regs.allocTemp()
|
|
575
|
+
const stateSlot = state.regs.allocTemp()
|
|
576
|
+
const ctrlSlot = state.regs.allocTemp()
|
|
577
|
+
this.emit(state, Opcode.MOVE, fnSlot, base, 0)
|
|
578
|
+
this.emit(state, Opcode.MOVE, stateSlot, base + 1, 0)
|
|
579
|
+
this.emit(state, Opcode.MOVE, ctrlSlot, base + 2, 0)
|
|
580
|
+
this.emit(state, Opcode.CALL, fnSlot, 3, nVars + 1)
|
|
581
|
+
state.regs.freeTemp(callBase)
|
|
582
|
+
|
|
583
|
+
const resultRegs: number[] = []
|
|
584
|
+
for (let i = 0; i < nVars; i++) resultRegs.push(state.regs.declareLocal())
|
|
585
|
+
const firstResult = resultRegs[0]
|
|
586
|
+
|
|
587
|
+
this.emit(state, Opcode.TEST, firstResult, 0, 1) // skip next(jump-away) when truthy
|
|
588
|
+
const exitJmp = this.emit(state, Opcode.JMP, 0, 0, 0, "generic-for-exit")
|
|
589
|
+
this.emit(state, Opcode.MOVE, base + 2, firstResult, 0)
|
|
590
|
+
|
|
591
|
+
for (let i = 0; i < nVars; i++) {
|
|
592
|
+
const bindingId = this.bindingIdOfDecl(stmt.variables[i])
|
|
593
|
+
if (this.captured.has(bindingId)) {
|
|
594
|
+
const boxReg = state.regs.declareLocal()
|
|
595
|
+
state.localSlots.set(bindingId, { reg: boxReg, boxed: true })
|
|
596
|
+
this.emitNewBox(state, boxReg)
|
|
597
|
+
const k = this.konst(state, BOX_FIELD)
|
|
598
|
+
this.emit(state, Opcode.SETTABLE, boxReg, RK(k, true), resultRegs[i])
|
|
599
|
+
} else {
|
|
600
|
+
state.localSlots.set(bindingId, { reg: resultRegs[i], boxed: false })
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
state.breakPatchStack.push([])
|
|
605
|
+
state.continuePatchStack.push([])
|
|
606
|
+
this.compileBlock(stmt.body, state)
|
|
607
|
+
this.emit(state, Opcode.JMP, loopStart - state.proto.code.length, 0, 0, "generic-for-back")
|
|
608
|
+
const endIdx = state.proto.code.length
|
|
609
|
+
this.patchJump(state, exitJmp, endIdx)
|
|
610
|
+
for (const b of state.breakPatchStack.pop()!) this.patchJump(state, b, endIdx)
|
|
611
|
+
for (const c of state.continuePatchStack.pop()!) this.patchJump(state, c, loopStart)
|
|
612
|
+
|
|
613
|
+
state.regs.releaseLocalsTo(saved)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private compileCallMultiInto(callExpr: CallExpression, state: FuncState, wantCount: number): number {
|
|
617
|
+
// CallExpression과 동일한 이유로 top()을 미리 캐싱하지 않고 callee의 반환값을 base로 씀.
|
|
618
|
+
const base = this.compileExpr(callExpr.callee, state)
|
|
619
|
+
this.compileArgsContiguous(callExpr.arguments, state, base + 1)
|
|
620
|
+
this.emit(state, Opcode.CALL, base, callExpr.arguments.length + 1, wantCount + 1)
|
|
621
|
+
return base
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private loadConst(state: FuncState, value: ConstValue): number {
|
|
625
|
+
const reg = state.regs.allocTemp()
|
|
626
|
+
const k = this.konst(state, value)
|
|
627
|
+
this.emit(state, Opcode.LOADK, reg, k, 0)
|
|
628
|
+
return reg
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private patchJump(state: FuncState, instrIdx: number, targetIdx: number): void {
|
|
632
|
+
this.patchJumpField(state, instrIdx, targetIdx, "a")
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private patchJumpField(state: FuncState, instrIdx: number, targetIdx: number, field: "a" | "b"): void {
|
|
636
|
+
state.proto.code[instrIdx][field] = targetIdx - instrIdx
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private bindingIdOfDecl(node: object): BindingId {
|
|
640
|
+
const id = this.declToBinding.get(node)
|
|
641
|
+
if (id === undefined) throw new Error("vmify: could not resolve declaration binding (compiler bug)")
|
|
642
|
+
return id
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** expr을 컴파일해서 결과가 정확히 target 레지스터에 오도록 강제(필요하면 MOVE 한 번 추가). */
|
|
646
|
+
private compileExprTo(expr: Expression, state: FuncState, target: number): void {
|
|
647
|
+
const r = this.compileExpr(expr, state)
|
|
648
|
+
if (r !== target) this.emit(state, Opcode.MOVE, target, r, 0)
|
|
649
|
+
state.regs.freeTemp(target + 1)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* CALL/RETURN처럼 "레지스터 base부터 연속으로 n개"를 요구하는 명령을 위해, 인자 표현식
|
|
654
|
+
* 목록을 base, base+1, base+2 ...에 강제로 배치한다. 단순히 순서대로 compileExpr만
|
|
655
|
+
* 호출하면 개별 인자는 각자 "자기 자신의 결과+1"이 다음 top이 되는 게 맞지만, 그 인자가
|
|
656
|
+
* 복합 표현식(예: 이항연산 `i > 2`)이라 내부에서 스크래치 레지스터를 여러 개 쓰면 다음
|
|
657
|
+
* 인자와의 사이에 빈틈이 생겨 CALL/RETURN이 잘못된 레지스터를 읽게 된다 — 그래서 매
|
|
658
|
+
* 인자마다 목표 레지스터를 명시하고 필요하면 MOVE로 맞춰준다.
|
|
659
|
+
*/
|
|
660
|
+
private compileArgsContiguous(exprs: Expression[], state: FuncState, base: number): void {
|
|
661
|
+
for (let i = 0; i < exprs.length; i++) this.compileExprTo(exprs[i], state, base + i)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Call/MethodCall/Vararg처럼 "여러 값"을 낼 수 있는 표현식인지. */
|
|
665
|
+
private isMultiValueExpr(expr: Expression): boolean {
|
|
666
|
+
return expr.type === "CallExpression" || expr.type === "MethodCallExpression" || expr.type === "VarargExpression"
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* expr을 컴파일해서 "정확히 wantCount개"의 값을 base..base+wantCount-1에 채운다
|
|
671
|
+
* (모자라면 nil로 패딩). Call/MethodCall/Vararg만 1개 이상을 낼 수 있고, 그 외
|
|
672
|
+
* 표현식은 항상 1개만 내므로 나머지는 자동으로 nil 패딩된다.
|
|
673
|
+
*/
|
|
674
|
+
private compileExprMultiInto(expr: Expression, state: FuncState, base: number, wantCount: number): void {
|
|
675
|
+
if (wantCount <= 0) return
|
|
676
|
+
if (expr.type === "CallExpression") {
|
|
677
|
+
const calleeReg = this.compileExpr(expr.callee, state)
|
|
678
|
+
const b = this.compileCallArgsAndGetB(expr.arguments, state, calleeReg + 1)
|
|
679
|
+
this.emit(state, Opcode.CALL, calleeReg, b, wantCount + 1)
|
|
680
|
+
for (let i = 0; i < wantCount; i++) {
|
|
681
|
+
if (calleeReg + i !== base + i) this.emit(state, Opcode.MOVE, base + i, calleeReg + i, 0)
|
|
682
|
+
}
|
|
683
|
+
state.regs.freeTemp(base + wantCount)
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
if (expr.type === "MethodCallExpression") {
|
|
687
|
+
const objReg = this.compileExpr(expr.object, state)
|
|
688
|
+
const k = this.konst(state, expr.method.name)
|
|
689
|
+
const fnSlot = state.regs.allocTemp()
|
|
690
|
+
this.emit(state, Opcode.SELF, fnSlot, objReg, RK(k, true))
|
|
691
|
+
state.regs.allocTemp()
|
|
692
|
+
const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2)
|
|
693
|
+
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
694
|
+
this.emit(state, Opcode.CALL, fnSlot, b, wantCount + 1)
|
|
695
|
+
for (let i = 0; i < wantCount; i++) {
|
|
696
|
+
if (fnSlot + i !== base + i) this.emit(state, Opcode.MOVE, base + i, fnSlot + i, 0)
|
|
697
|
+
}
|
|
698
|
+
state.regs.freeTemp(base + wantCount)
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
if (expr.type === "VarargExpression") {
|
|
702
|
+
this.emit(state, Opcode.VARARG, base, wantCount + 1, 0)
|
|
703
|
+
state.regs.freeTemp(base + wantCount)
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
// 다중값 불가능 — 1개만 채우고 나머지는 nil.
|
|
707
|
+
this.compileExprTo(expr, state, base)
|
|
708
|
+
for (let i = 1; i < wantCount; i++) this.emit(state, Opcode.LOADNIL, base + i, base + i, 0)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* expr을 컴파일해서 "런타임이 결정하는 개수만큼" base부터 이어서 채운다(frame.multiTop
|
|
713
|
+
* 갱신). Call/MethodCall/Vararg가 아니면 그냥 1개로 취급(compileExprTo와 동일).
|
|
714
|
+
* `return f()`나 `g(a, f())`의 마지막 인자처럼 다중값을 그대로 전파할 때 씀.
|
|
715
|
+
*/
|
|
716
|
+
private compileExprOpenInto(expr: Expression, state: FuncState, base: number): void {
|
|
717
|
+
if (expr.type === "CallExpression") {
|
|
718
|
+
// 결과 개수를 컴파일 타임에 모르므로(=open) CALL 이후엔 값들을 재배치할 수 없다.
|
|
719
|
+
// 그래서 CALL "전에" callee 자체를 base로 강제 이동시켜, CALL이 쓰는 'a'가
|
|
720
|
+
// 곧 base가 되도록 만든다(그래야 열린 결과가 정확히 base부터 채워짐).
|
|
721
|
+
const calleeReg = this.compileExpr(expr.callee, state)
|
|
722
|
+
if (calleeReg !== base) this.emit(state, Opcode.MOVE, base, calleeReg, 0)
|
|
723
|
+
state.regs.freeTemp(base + 1)
|
|
724
|
+
const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1)
|
|
725
|
+
this.emit(state, Opcode.CALL, base, b, 0)
|
|
726
|
+
return
|
|
727
|
+
}
|
|
728
|
+
if (expr.type === "MethodCallExpression") {
|
|
729
|
+
const objReg = this.compileExpr(expr.object, state)
|
|
730
|
+
const k = this.konst(state, expr.method.name)
|
|
731
|
+
const fnSlot = state.regs.allocTemp()
|
|
732
|
+
this.emit(state, Opcode.SELF, fnSlot, objReg, RK(k, true))
|
|
733
|
+
state.regs.allocTemp() // fnSlot+1 = self
|
|
734
|
+
if (fnSlot !== base) {
|
|
735
|
+
this.emit(state, Opcode.MOVE, base, fnSlot, 0)
|
|
736
|
+
this.emit(state, Opcode.MOVE, base + 1, fnSlot + 1, 0)
|
|
737
|
+
}
|
|
738
|
+
state.regs.freeTemp(base + 2)
|
|
739
|
+
const argB = this.compileCallArgsAndGetB(expr.arguments, state, base + 2)
|
|
740
|
+
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
741
|
+
this.emit(state, Opcode.CALL, base, b, 0)
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
if (expr.type === "VarargExpression") {
|
|
745
|
+
this.emit(state, Opcode.VARARG, base, 0, 0)
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
this.compileExprTo(expr, state, base)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* 호출 인자 목록을 base부터 컴파일. 마지막 인자가 다중값 가능한 표현식이면 열어서
|
|
753
|
+
* 끝까지 전파(`f(a, g())`가 g()의 모든 반환값을 다 넘기도록)하고 CALL의 b로 쓸 값(0)을
|
|
754
|
+
* 반환한다. 아니면 고정 개수(b=args.length+1)를 반환.
|
|
755
|
+
*/
|
|
756
|
+
private compileCallArgsAndGetB(args: Expression[], state: FuncState, base: number): number {
|
|
757
|
+
if (args.length === 0) return 1
|
|
758
|
+
for (let i = 0; i < args.length - 1; i++) this.compileExprTo(args[i], state, base + i)
|
|
759
|
+
const last = args[args.length - 1]
|
|
760
|
+
const lastBase = base + args.length - 1
|
|
761
|
+
if (this.isMultiValueExpr(last)) {
|
|
762
|
+
this.compileExprOpenInto(last, state, lastBase)
|
|
763
|
+
return 0
|
|
764
|
+
}
|
|
765
|
+
this.compileExprTo(last, state, lastBase)
|
|
766
|
+
return args.length + 1
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
private compileExpr(expr: Expression, state: FuncState, discard = false): number {
|
|
770
|
+
switch (expr.type) {
|
|
771
|
+
case "NilLiteral": {
|
|
772
|
+
const r = state.regs.allocTemp()
|
|
773
|
+
this.emit(state, Opcode.LOADNIL, r, r, 0)
|
|
774
|
+
return r
|
|
775
|
+
}
|
|
776
|
+
case "BooleanLiteral": {
|
|
777
|
+
const r = state.regs.allocTemp()
|
|
778
|
+
this.emit(state, Opcode.LOADBOOL, r, expr.value ? 1 : 0, 0)
|
|
779
|
+
return r
|
|
780
|
+
}
|
|
781
|
+
case "NumberLiteral":
|
|
782
|
+
return this.loadConst(state, expr.value)
|
|
783
|
+
case "StringLiteral":
|
|
784
|
+
return this.loadConst(state, expr.value)
|
|
785
|
+
case "Identifier": {
|
|
786
|
+
const bindingId = this.bindingIdOf(expr)
|
|
787
|
+
if (bindingId === undefined) throw new Error("vmify: unresolved identifier")
|
|
788
|
+
const r = state.regs.allocTemp()
|
|
789
|
+
this.emitLoadBinding(state, bindingId, r)
|
|
790
|
+
return r
|
|
791
|
+
}
|
|
792
|
+
case "ParenthesizedExpression":
|
|
793
|
+
return this.compileExpr(expr.expression, state)
|
|
794
|
+
case "TypeAssertionExpression":
|
|
795
|
+
return this.compileExpr(expr.expression, state)
|
|
796
|
+
case "BinaryExpression": {
|
|
797
|
+
if (expr.operator === "and" || expr.operator === "or") {
|
|
798
|
+
return this.compileLogical(expr.operator, expr.left, expr.right, state)
|
|
799
|
+
}
|
|
800
|
+
let leftExpr = expr.left
|
|
801
|
+
let rightExpr = expr.right
|
|
802
|
+
let op: Opcode
|
|
803
|
+
let swap = false
|
|
804
|
+
let negate = false
|
|
805
|
+
switch (expr.operator) {
|
|
806
|
+
case "+": op = Opcode.ADD; break
|
|
807
|
+
case "-": op = Opcode.SUB; break
|
|
808
|
+
case "*": op = Opcode.MUL; break
|
|
809
|
+
case "/": op = Opcode.DIV; break
|
|
810
|
+
case "//": op = Opcode.IDIV; break
|
|
811
|
+
case "%": op = Opcode.MOD; break
|
|
812
|
+
case "^": op = Opcode.POW; break
|
|
813
|
+
case "..": op = Opcode.CONCAT; break
|
|
814
|
+
case "==": op = Opcode.EQ; break
|
|
815
|
+
case "~=": op = Opcode.EQ; negate = true; break
|
|
816
|
+
case "<": op = Opcode.LT; break
|
|
817
|
+
case "<=": op = Opcode.LE; break
|
|
818
|
+
case ">": op = Opcode.LT; swap = true; break
|
|
819
|
+
case ">=": op = Opcode.LE; swap = true; break
|
|
820
|
+
default:
|
|
821
|
+
throw new Error(`vmify: unsupported binary operator ${expr.operator}`)
|
|
822
|
+
}
|
|
823
|
+
if (swap) { const t = leftExpr; leftExpr = rightExpr; rightExpr = t }
|
|
824
|
+
const l = this.compileExpr(leftExpr, state)
|
|
825
|
+
const r = this.compileExpr(rightExpr, state)
|
|
826
|
+
const dest = state.regs.allocTemp()
|
|
827
|
+
this.emit(state, op, dest, l, r)
|
|
828
|
+
if (negate) this.emit(state, Opcode.NOT, dest, dest, 0)
|
|
829
|
+
state.regs.freeTemp(dest + 1)
|
|
830
|
+
return dest
|
|
831
|
+
}
|
|
832
|
+
case "UnaryExpression": {
|
|
833
|
+
const argReg = this.compileExpr(expr.argument, state)
|
|
834
|
+
const dest = state.regs.allocTemp()
|
|
835
|
+
const op = expr.operator === "-" ? Opcode.UNM : expr.operator === "not" ? Opcode.NOT : Opcode.LEN
|
|
836
|
+
this.emit(state, op, dest, argReg, 0)
|
|
837
|
+
state.regs.freeTemp(dest + 1)
|
|
838
|
+
return dest
|
|
839
|
+
}
|
|
840
|
+
case "TableExpression": {
|
|
841
|
+
const dest = state.regs.allocTemp()
|
|
842
|
+
this.emit(state, Opcode.NEWTABLE, dest, 0, 0)
|
|
843
|
+
let posIndex = 1
|
|
844
|
+
for (let fi = 0; fi < expr.fields.length; fi++) {
|
|
845
|
+
const field = expr.fields[fi]
|
|
846
|
+
const isLast = fi === expr.fields.length - 1
|
|
847
|
+
if (field.type === "TableFieldPositional") {
|
|
848
|
+
// 마지막 positional 필드가 Call/MethodCall/Vararg면 다중값을 전부
|
|
849
|
+
// 펼쳐서 posIndex, posIndex+1, ...에 채운다 (`{...}`, `{f()}`).
|
|
850
|
+
if (isLast && this.isMultiValueExpr(field.value)) {
|
|
851
|
+
const valuesBase = state.regs.allocTemp()
|
|
852
|
+
this.compileExprOpenInto(field.value, state, valuesBase)
|
|
853
|
+
this.emit(state, Opcode.SETLIST, dest, valuesBase, posIndex)
|
|
854
|
+
state.regs.freeTemp(valuesBase)
|
|
855
|
+
} else {
|
|
856
|
+
const v = this.compileExpr(field.value, state)
|
|
857
|
+
const k = this.konst(state, posIndex++)
|
|
858
|
+
this.emit(state, Opcode.SETTABLE, dest, RK(k, true), v)
|
|
859
|
+
state.regs.freeTemp(v)
|
|
860
|
+
}
|
|
861
|
+
} else if (field.type === "TableFieldNamed") {
|
|
862
|
+
const v = this.compileExpr(field.value, state)
|
|
863
|
+
const k = this.konst(state, field.name.name)
|
|
864
|
+
this.emit(state, Opcode.SETTABLE, dest, RK(k, true), v)
|
|
865
|
+
state.regs.freeTemp(v)
|
|
866
|
+
} else {
|
|
867
|
+
const k = this.compileExpr(field.key, state)
|
|
868
|
+
const v = this.compileExpr(field.value, state)
|
|
869
|
+
this.emit(state, Opcode.SETTABLE, dest, k, v)
|
|
870
|
+
state.regs.freeTemp(k)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return dest
|
|
874
|
+
}
|
|
875
|
+
case "MemberExpression": {
|
|
876
|
+
const objReg = this.compileExpr(expr.object, state)
|
|
877
|
+
const k = this.konst(state, expr.property.name)
|
|
878
|
+
const dest = state.regs.allocTemp()
|
|
879
|
+
this.emit(state, Opcode.GETTABLE, dest, objReg, RK(k, true))
|
|
880
|
+
state.regs.freeTemp(dest + 1)
|
|
881
|
+
return dest
|
|
882
|
+
}
|
|
883
|
+
case "IndexExpression": {
|
|
884
|
+
const objReg = this.compileExpr(expr.object, state)
|
|
885
|
+
const keyReg = this.compileExpr(expr.index, state)
|
|
886
|
+
const dest = state.regs.allocTemp()
|
|
887
|
+
this.emit(state, Opcode.GETTABLE, dest, objReg, keyReg)
|
|
888
|
+
state.regs.freeTemp(dest + 1)
|
|
889
|
+
return dest
|
|
890
|
+
}
|
|
891
|
+
case "CallExpression": {
|
|
892
|
+
// 주의: base는 반드시 compileExpr(callee)의 "반환값"이어야 한다 — top()을
|
|
893
|
+
// 미리 캐싱하면 안 됨. callee가 MemberExpression/IndexExpression처럼 내부에서
|
|
894
|
+
// 스크래치 레지스터를 소모하는 표현식이면 실제 함수 값은 사전 계산한 top()보다
|
|
895
|
+
// 한 칸 이상 위에 놓이기 때문(예: `Account.new(...)`에서 Account를 읽는 데
|
|
896
|
+
// 쓴 임시 레지스터 때문에 함수 값 자체는 그 다음 슬롯에 옴).
|
|
897
|
+
const base = this.compileExpr(expr.callee, state)
|
|
898
|
+
const b = this.compileCallArgsAndGetB(expr.arguments, state, base + 1)
|
|
899
|
+
this.emit(state, Opcode.CALL, base, b, discard ? 1 : 2)
|
|
900
|
+
state.regs.freeTemp(base + (discard ? 0 : 1))
|
|
901
|
+
return base
|
|
902
|
+
}
|
|
903
|
+
case "MethodCallExpression": {
|
|
904
|
+
const objReg = this.compileExpr(expr.object, state)
|
|
905
|
+
const k = this.konst(state, expr.method.name)
|
|
906
|
+
const fnSlot = state.regs.allocTemp()
|
|
907
|
+
this.emit(state, Opcode.SELF, fnSlot, objReg, RK(k, true))
|
|
908
|
+
state.regs.allocTemp()
|
|
909
|
+
const argB = this.compileCallArgsAndGetB(expr.arguments, state, fnSlot + 2)
|
|
910
|
+
// self가 항상 고정으로 하나 더 붙으므로: 열려있으면(0) 그대로 0(런타임이
|
|
911
|
+
// multiTop까지 다 잡아줌, self도 그 범위 안에 포함됨), 아니면 +1(self 몫).
|
|
912
|
+
const b = argB === 0 ? 0 : expr.arguments.length + 2
|
|
913
|
+
this.emit(state, Opcode.CALL, fnSlot, b, discard ? 1 : 2)
|
|
914
|
+
state.regs.freeTemp(fnSlot + (discard ? 0 : 1))
|
|
915
|
+
return fnSlot
|
|
916
|
+
}
|
|
917
|
+
case "FunctionExpression": {
|
|
918
|
+
const protoIdx = this.compileFunctionBody(expr.func, state)
|
|
919
|
+
const dest = state.regs.allocTemp()
|
|
920
|
+
this.emitClosure(state, dest, protoIdx)
|
|
921
|
+
return dest
|
|
922
|
+
}
|
|
923
|
+
case "InterpolatedStringExpression": {
|
|
924
|
+
let acc: number | null = null
|
|
925
|
+
for (const part of expr.parts) {
|
|
926
|
+
const partReg = part.kind === "string"
|
|
927
|
+
? this.loadConst(state, part.value)
|
|
928
|
+
: this.compileExpr(part.expression, state)
|
|
929
|
+
if (acc === null) {
|
|
930
|
+
acc = partReg
|
|
931
|
+
} else {
|
|
932
|
+
const dest = state.regs.allocTemp()
|
|
933
|
+
this.emit(state, Opcode.CONCAT, dest, acc, partReg)
|
|
934
|
+
state.regs.freeTemp(dest + 1)
|
|
935
|
+
acc = dest
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return acc ?? this.loadConst(state, "")
|
|
939
|
+
}
|
|
940
|
+
case "VarargExpression": {
|
|
941
|
+
const r = state.regs.allocTemp()
|
|
942
|
+
this.emit(state, Opcode.VARARG, r, 2, 0, "vararg (1 value)")
|
|
943
|
+
return r
|
|
944
|
+
}
|
|
945
|
+
case "IfElseExpression": {
|
|
946
|
+
const dest = state.regs.allocTemp()
|
|
947
|
+
const endPatches: number[] = []
|
|
948
|
+
for (const clause of expr.clauses) {
|
|
949
|
+
const condReg = this.compileExpr(clause.condition, state)
|
|
950
|
+
this.emit(state, Opcode.TEST, condReg, 0, 1) // skip next(jump-away) when truthy
|
|
951
|
+
const skip = this.emit(state, Opcode.JMP, 0, 0, 0)
|
|
952
|
+
state.regs.freeTemp(condReg)
|
|
953
|
+
const bodyReg = this.compileExpr(clause.body, state)
|
|
954
|
+
if (bodyReg !== dest) this.emit(state, Opcode.MOVE, dest, bodyReg, 0)
|
|
955
|
+
state.regs.freeTemp(dest + 1)
|
|
956
|
+
const toEnd = this.emit(state, Opcode.JMP, 0, 0, 0)
|
|
957
|
+
endPatches.push(toEnd)
|
|
958
|
+
this.patchJump(state, skip, state.proto.code.length)
|
|
959
|
+
}
|
|
960
|
+
const altReg = this.compileExpr(expr.alternate, state)
|
|
961
|
+
if (altReg !== dest) this.emit(state, Opcode.MOVE, dest, altReg, 0)
|
|
962
|
+
state.regs.freeTemp(dest + 1)
|
|
963
|
+
for (const p of endPatches) this.patchJump(state, p, state.proto.code.length)
|
|
964
|
+
return dest
|
|
965
|
+
}
|
|
966
|
+
default: {
|
|
967
|
+
const _exhaustive: never = expr
|
|
968
|
+
throw new Error(`vmify: unhandled expression ${(_exhaustive as Expression).type}`)
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private compileLogical(operator: "and" | "or", left: Expression, right: Expression, state: FuncState): number {
|
|
974
|
+
const dest = this.compileExpr(left, state)
|
|
975
|
+
// and: 왼쪽이 falsy면 단락(오른쪽 평가 JMP를 반드시 실행) -> "JMP가 실행돼야 함"은
|
|
976
|
+
// skip-조건이 falsy일 때 거짓이어야 한다는 뜻 -> skip-on-truthy -> c=1.
|
|
977
|
+
// or: 왼쪽이 truthy면 단락 -> 반대로 skip-on-falsy -> c=0.
|
|
978
|
+
this.emit(state, Opcode.TEST, dest, 0, operator === "and" ? 1 : 0, `${operator} short-circuit`)
|
|
979
|
+
const skip = this.emit(state, Opcode.JMP, 0, 0, 0)
|
|
980
|
+
state.regs.freeTemp(dest)
|
|
981
|
+
const rightReg = this.compileExpr(right, state)
|
|
982
|
+
if (rightReg !== dest) this.emit(state, Opcode.MOVE, dest, rightReg, 0)
|
|
983
|
+
this.patchJump(state, skip, state.proto.code.length)
|
|
984
|
+
state.regs.freeTemp(dest + 1)
|
|
985
|
+
return dest
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private emitClosure(state: FuncState, dest: number, protoIdx: number): void {
|
|
989
|
+
this.emit(state, Opcode.CLOSURE, dest, protoIdx, 0)
|
|
990
|
+
const proto = state.proto.protos[protoIdx]
|
|
991
|
+
for (const desc of proto.upvalDescs) {
|
|
992
|
+
if (desc.kind === "local") this.emit(state, Opcode.MOVE, 0, desc.index, 0, "capture local")
|
|
993
|
+
else this.emit(state, Opcode.GETUPVAL, 0, desc.index, 0, "capture upval")
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
private compileFunctionBody(fb: FunctionBody, parent: FuncState): number {
|
|
998
|
+
const marker = ownerFunctionOf(this.enclosing, fb)
|
|
999
|
+
const child = this.newFuncState(parent, marker)
|
|
1000
|
+
child.proto.numParams = fb.params.length
|
|
1001
|
+
child.proto.hasVarargs = fb.hasVarargs
|
|
1002
|
+
// 자연 파라미터 레지스터(0..numParams-1)를 "전부 먼저" 확정해야 한다 — 런타임
|
|
1003
|
+
// 프롤로그가 정확히 그 범위에 인자를 채워 넣기 때문(R[i-1]=args[i]). 캡처되는
|
|
1004
|
+
// 파라미터가 있다고 해서 중간에 박스 레지스터를 끼워 넣으면 그 뒤 파라미터들의
|
|
1005
|
+
// 자연 슬롯 번호가 밀려서 호출 규약이 깨진다 — 그래서 박스는 반드시 "두 번째 패스"에서,
|
|
1006
|
+
// 이미 확정된 자연 레지스터 값을 따로 옮겨 담는 방식으로 만든다.
|
|
1007
|
+
const naturalRegs: number[] = fb.params.map(() => child.regs.declareLocal())
|
|
1008
|
+
for (let i = 0; i < fb.params.length; i++) {
|
|
1009
|
+
const bindingId = this.bindingIdOfDecl(fb.params[i])
|
|
1010
|
+
if (this.captured.has(bindingId)) {
|
|
1011
|
+
const boxReg = child.regs.declareLocal()
|
|
1012
|
+
this.emitNewBox(child, boxReg)
|
|
1013
|
+
const k = this.konst(child, BOX_FIELD)
|
|
1014
|
+
this.emit(child, Opcode.SETTABLE, boxReg, RK(k, true), naturalRegs[i])
|
|
1015
|
+
child.localSlots.set(bindingId, { reg: boxReg, boxed: true })
|
|
1016
|
+
} else {
|
|
1017
|
+
child.localSlots.set(bindingId, { reg: naturalRegs[i], boxed: false })
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
this.compileBlock(fb.body, child)
|
|
1021
|
+
this.emit(child, Opcode.RETURN, 0, 1, 0)
|
|
1022
|
+
child.proto.maxRegs = child.regs.maxUsed
|
|
1023
|
+
parent.proto.protos.push(child.proto)
|
|
1024
|
+
return parent.proto.protos.length - 1
|
|
1025
|
+
}
|
|
1026
|
+
}
|