Luau Bytecode EXPLAINED - How to Read, Debug, and Optimize Like a Hacker

Would you read a wall of text without going insane?
I think that you won’t read a boring wall of text, and hence why I kept the humorous “all sunshine and rainbows” theme of the topic.
Accusing someone of using AI because of emojis is insane, man.

1 Like

A big part of the luau bytecode compiler being dumb is that the compiler itself needs to be extremely fast because Roblox recompiles bytecode each server startup

So the actual source code gets stored and then on RCC start it will compile all the bytecode. That means you can’t do fancy stuff

W post though

3 Likes

Thank you so much for making this because I am making Luau VM in JS and it is super helpful

2 Likes

*Bytecode.h Lil broooo :pray:


Later,

Later,

Even more later,

Previously:


0 complexity, 0 presentation
“Frreee Executor Roblox”


What is this supposed to mean lil bro


What a good presentation
Letting your users think it is malware at first glance and providing download links in a Discord server? Good stuff


…?


3 Likes

I will not gatekeep like this Dottik noob So here guys
Hopefully it helps somebody

// add luau tuple support with js array (i think it's supported but just in case)
// dupclosure is a little messy so fix it
// work on namecall and call
import ByteEditor from "/modules/ByteEditor.js";
import { enforcePropertyConstructor, randomString } from "/modules/utilities.js";

const opList = [
	["NOP", 0, 0, false],
	["BREAK", 0, 0, false],
	["LOADNIL", 1, 0, false],
	["LOADB", 3, 0, false],
	["LOADN", 4, 0, false],
	["LOADK", 4, 3, false],
	["MOVE", 2, 0, false],
	["GETGLOBAL", 1, 1, true],
	["SETGLOBAL", 1, 1, true],
	["GETUPVAL", 2, 0, false],
	["SETUPVAL", 2, 0, false],
	["CLOSEUPVALS", 1, 0, false],
	["GETIMPORT", 4, 4, true],
	["GETTABLE", 3, 0, false],
	["SETTABLE", 3, 0, false],
	["GETTABLEKS", 3, 1, true],
	["SETTABLEKS", 3, 1, true],
	["GETTABLEN", 3, 0, false],
	["SETTABLEN", 3, 0, false],
	["NEWCLOSURE", 4, 0, false],
	["NAMECALL", 3, 1, true],
	["CALL", 3, 0, false],
	["RETURN", 2, 0, false],
	["JUMP", 4, 0, false],
	["JUMPBACK", 4, 0, false],
	["JUMPIF", 4, 0, false],
	["JUMPIFNOT", 4, 0, false],
	["JUMPIFEQ", 4, 0, true],
	["JUMPIFLE", 4, 0, true],
	["JUMPIFLT", 4, 0, true],
	["JUMPIFNOTEQ", 4, 0, true],
	["JUMPIFNOTLE", 4, 0, true],
	["JUMPIFNOTLT", 4, 0, true],
	["ADD", 3, 0, false],
	["SUB", 3, 0, false],
	["MUL", 3, 0, false],
	["DIV", 3, 0, false],
	["MOD", 3, 0, false],
	["POW", 3, 0, false],
	["ADDK", 3, 2, false],
	["SUBK", 3, 2, false],
	["MULK", 3, 2, false],
	["DIVK", 3, 2, false],
	["MODK", 3, 2, false],
	["POWK", 3, 2, false],
	["AND", 3, 0, false],
	["OR", 3, 0, false],
	["ANDK", 3, 2, false],
	["ORK", 3, 2, false],
	["CONCAT", 3, 0, false],
	["NOT", 2, 0, false],
	["MINUS", 2, 0, false],
	["LENGTH", 2, 0, false],
	["NEWTABLE", 2, 0, true],
	["DUPTABLE", 4, 3, false],
	["SETLIST", 3, 0, true],
	["FORNPREP", 4, 0, false],
	["FORNLOOP", 4, 0, false],
	["FORGLOOP", 4, 8, true],
	["FORGPREP_INEXT", 4, 0, false],
	["FASTCALL3", 3, 1, true],
	["FORGPREP_NEXT", 4, 0, false],
	["DEP_FORGLOOP_NEXT", 0, 0, false],
	["GETVARARGS", 2, 0, false],
	["DUPCLOSURE", 4, 9, false],
	["PREPVARARGS", 1, 0, false],
	["LOADKX", 1, 1, true],
	["JUMPX", 5, 0, false],
	["FASTCALL", 3, 0, false],
	["COVERAGE", 5, 0, false],
	["CAPTURE", 2, 0, false],
	["SUBRK", 3, 7, false],
	["DIVRK", 3, 7, false],
	["FASTCALL1", 3, 0, false],
	["FASTCALL2", 3, 0, true],
	["FASTCALL2K", 3, 1, true],
	["FORGPREP", 4, 0, false],
	["JUMPXEQKNIL", 4, 5, true],
	["JUMPXEQKB", 4, 5, true],
	["JUMPXEQKN", 4, 6, true],
	["JUMPXEQKS", 4, 6, true],
	["IDIV", 3, 0, false],
	["IDIVK", 3, 2, false],
];

const LUA_MULTRET = -1;
// const LUA_GENERALIZED_TERMINATOR = -2; // unneeded?

function getLuauType(object) {
	if (object == null) return "nil";

	switch (typeof object) {
		case "bigint":
		case "number": return "number";
		case "string": return "string";
		case "boolean": return "boolean";
		case "object": return "table";
		case "function": return "function";
		case "symbol":
		default: return "userdata"; // maybe
	};
};

// const bit_band = (a, b) => (a & b) >>> 0;
// const bit_rshift = (v, s) => (v >>> s) >>> 0;
const bit_extract = (v, pos, size) => ((v >>> pos) & ((1 << size) - 1)) >>> 0;

export default class LuauEngine {
	name = `[string "${randomString(7, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopwrstuvwxyz1234567890")}"]`;
	globals = Object.create(null);
	useImportConstants = false;
	vectorConstructor = () => { throw new Error("Vector not implemented yet") };

	constructor() {
		enforcePropertyConstructor(this, "name", String);
		enforcePropertyConstructor(this, "globals", Object);
		enforcePropertyConstructor(this, "useImportConstants", Boolean);
		[
			"vectorConstructor",
			"bindToLuauThis"
		].forEach(name => enforcePropertyConstructor(this, name, Function));
	}

	bindToLuauThis(jsFunction) {
		return function (self, ...args) {
			return jsFunction.apply(self, args);
		}
	}

	deserialize(bytecode) {
		const engine = this;
		const { useImportConstants, globals } = engine;
		const editor = new ByteEditor(bytecode);

		const luauVersion = editor.readUint8();
		let typesVersion = 0;
		if (luauVersion === 0) {
			throw new SyntaxError(engine.name + editor.readString(editor.bytesLeft)); // new Error("the provided bytecode is an error message");
		} else if (luauVersion < 3 || luauVersion > 6) {
			throw new TypeError(`unsupported luau bytecode, version ${luauVersion}`);
		} else if (luauVersion >= 4) {
			typesVersion = editor.readUint8();
		}

		const stringCount = editor.readVarInt32();
		const stringList = Array.from({ length: stringCount }, () =>
			editor.readString(editor.readVarInt32()) // stored indices in bytecode are 1-based -> subtract 1 when used; here we read strings sequentially
		); // 0-based

		function readInstruction(codeList) {
			const value = editor.readUint32(true); // decodeOp
			const opcode = (value & 0xFF);

			const opinfo = opList[opcode];
			if (!opinfo) throw new TypeError("Unknown opcode " + opcode);
			const [opname, opmode, kmode, usesAux] = opinfo;

			const inst = {
				opcode,
				opname,
				opmode,
				kmode,
				usesAux
			};

			codeList.push(inst);

			switch (opmode) {
				case 1: inst.A = ((value >>> 8) & 0xFF); break; // A
				case 2: { // AB
					inst.A = ((value >>> 8) & 0xFF);
					inst.B = ((value >>> 16) & 0xFF);
					break;
				};
				case 3: { // ABC
					inst.A = ((value >>> 8) & 0xFF);
					inst.B = ((value >>> 16) & 0xFF);
					inst.C = ((value >>> 24) & 0xFF);
				};
				case 4: { // AD
					inst.A = ((value >>> 8) & 0xFF);
					const temp = ((value >>> 16) & 0xFFFF);
					inst.D = (temp < 0x8000) ? temp : (temp - 0x10000);
					break;
				};
				case 5: { // AE
					const temp = ((value >>> 8) & 0xFFFFFF);
					inst.E = (temp < 0x800000) ? temp : (temp - 0x1000000);
					break;
				}
			}

			if (usesAux) {
				const aux = editor.readUint32(true);
				inst.aux = aux;
				// mirror Lua behaviour of adding aux pseudo (so codeList lengths match)
				codeList.push({ value: aux, opname: "auxvalue" });
			}

			return usesAux;
		};

		function checkkmode(inst, k) {
			const { kmode, aux, C, D, K0, K1, K2 } = inst;
			switch (kmode) {
				case 1: inst.K = k[aux]; break; // AUX
				case 2: inst.K = k[C]; break; // C
				case 3: inst.K = k[D]; break; // D
				case 4: { // AUX import
					// const extend = aux;
					const count = (aux >>> 30);
					const id0 = ((aux >>> 20) & 0x3FF);

					inst.K0 = k[id0];
					inst.KC = count;

					switch (count) {
						// case 1?

						case 2: {
							const id1 = ((aux >>> 10) & 0x3FF);
							inst.K1 = k[id1];
							break
						};

						case 3: {
							const id1 = ((aux >>> 10) & 0x3FF);
							const id2 = ((aux >>> 0) & 0x3FF);
							inst.K1 = k[id1];
							inst.K2 = k[id2];
							break;
						};
					};

					if (useImportConstants) {
						let res = globals[K0];
						if (count > 1) res = res?.[K1];
						if (count > 2) res = res?.[K2];
						inst.K = res;
						/*
						function resolveImportConstant(staticEnv, count, k0, k1, k2) {
							let res = staticEnv[k0];
							if (count < 2 || res == null) return res;
							res = res[k1];
							if (count < 3 || res == null) return res;
							res = res[k2];
							return res;
						};

						inst.K = resolveImportConstant(
							globals,
							count, inst.K0, inst.K1, inst.K2
						);
						*/
					};

					break;
				};

				case 5: { // AUX boolean low 1 bit
					inst.K = (bit_extract(aux, 0, 1) === 1);
					inst.KN = (bit_extract(aux, 31, 1) === 1);
					break;
				};

				case 6: { // AUX number low 24 bits
					inst.K = k[bit_extract(aux, 0, 24)];
					inst.KN = (bit_extract(aux, 31, 1) === 1);
					break;
				};

				case 7: inst.K = k[B]; break; // B
				case 8: inst.K = (aux & 0xF); break; // AUX number low 16 bits
				case 9: inst.K = { Index: k[D], Closure: null, Upvalues: null }; break; // CLOSURE
			}
		}

		function readProto(bytecodeid) {
			const maxstacksize = editor.readUint8();
			const numparams = editor.readUint8();
			const nups = editor.readUint8();
			const isvararg = editor.readBoolean(); // editor.readUint8() !== 0;

			if (luauVersion >= 4) {
				editor.readUint8(); // flags
				const typesize = editor.readVarInt32();
				editor.move(typesize); // skip typesize bytes
			}

			const sizecode = editor.readVarInt32();
			const codelist = []; // 0-based

			let skipnext = false;
			for (let i = 0; i < sizecode; i++) {
				if (skipnext) {
					skipnext = false;
					continue;
				};

				skipnext = readInstruction(codelist);
			}

			// comebine codelist and debugcodelist init?
			const debugcodelist = Array.from({ length: sizecode }, (_, i) =>
				codelist[i].opcode
			);

			const sizek = editor.readVarInt32();
			const klist = Array.from({ length: sizek }, () => {
				const kt = editor.readUint8();

				switch (kt) {
					case 0: return undefined; // nil
					case 1: return editor.readBoolean(); // boolean
					case 2: return editor.readFloat64(true); // number (double?)
					case 3: return stringList[editor.readVarInt32() - 1]; // string
					case 4: return editor.readUint32(true); // import
					case 5: return Array.from({ length: editor.readVarInt32() }, () => // table
						editor.readVarInt32()
					);
					case 6: return editor.readVarInt32(); // Closure
					case 7: {
						// vectorSize
						const x = editor.readFloat32(true), y = editor.readFloat32(true), z = editor.readFloat32(true), w = editor.readFloat32(true);
						return engine.vectorConstructor(x, y, z, w); // await
					};
					default: throw new TypeError("Unknown constant type " + kt);
				};
			}); // 0-based

			// 2nd pass to replace constant references in the instruction
			for (let i = 0; i < codelist.length; i++) {
				checkkmode(codelist[i], klist);
			}

			const sizep = editor.readVarInt32();
			const protolist = Array.from({ length: sizep }, () =>
				editor.readVarInt32() // bytecode stores proto refs as 0-based typically; previous code added +1 — now keep raw
			); // 0-based

			const linedefined = editor.readVarInt32();
			const debugnameindex = editor.readVarInt32();
			const debugname = debugnameindex !== 0 ? stringList[debugnameindex - 1] : "(??)";

			// lineinfo
			const lineinfoenabled = editor.readBoolean(); // editor.readUint8() !== 0;
			let instructionlineinfo;

			if (lineinfoenabled) {
				const linegaplog2 = editor.readUint8();
				const intervals = ((sizecode - 1) >>> linegaplog2) + 1;

				let lastoffset = 0;
				const lineinfo = Array.from({ length: sizecode }, () =>
					(lastoffset += editor.readUint8())
				);

				let lastline = 0;
				const abslineinfo = Array.from({ length: intervals }, () =>
					(lastline += editor.readUint32(true)) >>> 0
				);

				instructionlineinfo = Array.from({ length: sizecode }, (_, i) => // base
					abslineinfo[i >>> linegaplog2] + lineinfo[i]
				);
			}

			// debuginfo optional
			if (editor.readBoolean()) { // editor.readUint8() !== 0
				const sizel = editor.readVarInt32();
				for (let i = 0; i < sizel; i++) {
					editor.readVarInt32();
					editor.readVarInt32();
					editor.readVarInt32();
					editor.readUint8();
				};

				const sizeupvalues = editor.readVarInt32();
				for (let i = 0; i < sizeupvalues; i++) {
					editor.readVarInt32();
				}
			}

			// Build prototype object (all 0-based)
			return {
				maxstacksize,
				numparams,
				nups,
				isvararg,
				linedefined,
				debugname,
				sizecode,
				code: codelist,               // 0-based
				debugcode: debugcodelist,    // 0-based
				sizek,
				k: klist,                    // 0-based
				sizep,
				protos: protolist,           // 0-based proto indices
				lineinfoenabled,
				instructionlineinfo,
				bytecodeid
			};
		}

		// userdataRemapping (not used)
		if (typesVersion === 3) {
			while (editor.readBoolean()) editor.readVarInt32();

			/*
			let index = editor.readUint8();
			while (index !== 0) {
				editor.readVarInt32();
				index = editor.readUint8();
			}
			*/
		}

		const protoCount = editor.readVarInt32();
		const protoList = Array.from({ length: protoCount }, (_, i) =>
			readProto(i)
		); // 0-based

		const mainProtoIndex = editor.readVarInt32();
		const mainProto = protoList[mainProtoIndex];

		if (editor.bytesLeft !== 0) {
			throw new Error("deserializer cursor position mismatch");
		}

		mainProto.debugname = "(main)";

		return {
			stringList,
			protoList,
			mainProto,
			typesVersion
		};
	};

	newclosure(module, proto = module.mainProto, upvals = []) {
		// async so that we can implement task.wait
		const engine = this;
		const { protoList } = module;
		const { numparams, maxstacksize, protos, code, debugcode: debugopcodes, k: constants, instructionlineinfo } = proto; // debugname
		const { globals, useImportConstants } = engine; // extensions

		class LuauError extends Error {
			constructor(message, pc) {
				const lineNumber = instructionlineinfo?.[pc] ?? -1;
				super(`${engine.name}:${lineNumber}: ${message}`);

				this.name = 'LuauError';
				this.lineNumber = lineNumber;
				if (Error.captureStackTrace) {
					Error.captureStackTrace(this, LuauError);
				}
			}
		};

		return async function (...args) {
			const varargs = []; // { len: 0, list: [] };

			const stack = new Array(maxstacksize); // .fill(undefined), length: Math.min(numparams, args.length)
			for (let i = 0; i < numparams && i < args.length; i++) {
				stack[i] = args[i];
			}

			if (numparams < args.length) {
				const start = numparams;
				const len = args.length - numparams;
				// varargs.len = len;
				for (let i = 0; i < len; i++) varargs[i] = args[start + i]; // varargs.list[i]
			}

			const debugging = { pc: 0, name: "NONE" };
			const open_upvalues = new Map();
			const generalized_iterators = new WeakMap(); // Map

			let handlingBreak = false;
			let top = -1, pc = 0;
			let inst, op;

			function getUpvalues(nups, duplicate) {
				const upvalues = Array.from({ length: nups }, () => {
					const { A: upvalueType, B: upvalueIndex } = code[pc++]; // pseudo = 

					switch (upvalueType) {
						case 0: { // value
							const upvalue = { value: stack[upvalueIndex], index: "value" };
							upvalue.store = upvalue;
							return upvalue;
						};

						case 1: { // reference
							if (!duplicate) {
								return open_upvalues.get(upvalueIndex) ?? // prev
									open_upvalues.set(upvalueIndex, {
										index: upvalueIndex,
										store: stack
									}).get(upvalueIndex);
							};
						};

						case 2: return upvals[upvalueIndex]; // upvalue
					}
				});

				return upvalues;
			}

			while (true) { // alive
				if (!handlingBreak) {
					inst = code[pc];
					if (!inst) throw new RangeError(`PC out of range: ${pc}`);
					op = inst.opcode;
				};

				handlingBreak = false;

				const { aux, opname, A, B, C, D, E, K, KC, K0, K1, K2, KN } = inst;

				debugging.pc = pc;
				debugging.top = top;
				debugging.name = opname;

				pc++;

				switch (op) { // opname
					case 0: break; // NOP
					case 1: { // BREAK
						pc -= 1;
						op = debugopcodes[pc];
						handlingBreak = true;
						break;
					}

					case 2: stack[A] = undefined; break; // LOADNIL

					case 3: { // LOADB
						stack[A] = B === 1;
						pc += C;
						break;
					}

					case 4: stack[A] = D; break; // LOADN
					case 5: stack[A] = K; break; // LOADK
					case 6: stack[A] = stack[B]; break; // MOVE

					case 7: { // GETGLOBAL
						stack[A] = globals[K]; // extensions[K] ||
						pc++;
						break;
					}

					case 8: { // SETGLOBAL
						globals[K] = stack[A];
						pc++;
						break;
					}

					case 9: { // GETUPVAL
						const uv = upvals[B];
						stack[A] = uv.store[uv.index];
						break;
					}

					case 10: { // SETUPVAL
						const uv = upvals[B];
						uv.store[uv.index] = stack[A];
						break;
					}

					case 11: { // CLOSEUPVALS
						// convert any open upvalues with index >= inst.A into closed upvalues
						for (const [idx, uv] of open_upvalues) {
							if (uv.index >= A) {
								uv.value = uv.store[uv.index];
								uv.store = uv;
								uv.index = "value";
								open_upvalues.delete(idx);
							}
						};

						break;
					}

					case 12: { // GETIMPORT
						if (useImportConstants) {
							stack[A] = K;
						} else {
							// count = KC
							const importWrap = globals[K0]; // extensions[k0] ||
							switch (KC) {
								case 1: stack[A] = importWrap; break;
								case 2: stack[A] = importWrap[K1]; break;
								case 3: stack[A] = importWrap[K1][K2]; break;
							};
						};

						pc++;
						break;
					}

					case 13: stack[A] = stack[B][stack[C]]; break; // GETTABLE
					case 14: stack[B][stack[C]] = stack[A]; break; // SETTABLE

					case 15: { // GETTABLEKS
						// index = K
						stack[A] = stack[B][K];
						pc++;
						break;
					}

					case 16: { // SETTABLEKS
						// index = K
						stack[B][K] = stack[A];
						pc++;
						break;
					}

					/*
					Luau table sequence is 1-based; incoming instruction used inst.C + 1 previously
					keep the language semantics (table[1] maps to obj[1] in JS). We assume sequences stored as JS arrays
					*/
					case 17: stack[A] = stack[B][C]; break; // GETTABLEN
					case 18: stack[B][C] = stack[A]; break; // SETTABLEN

					case 19: { // NEWCLOSURE
						// protos array holds indices into protoList; inst.D is the proto index
						const newPrototype = protoList[protos[D]];
						stack[A] = engine.newclosure(module, newPrototype, getUpvalues(newPrototype.nups, false));
						break;
					}

					// (TODO, make NAMECALL and CALL work with func(self, ...args) and func.apply(self, args))

					case 20: { // NAMECALL
						const obj = stack[B];
						const method = obj[K];
						stack[A] = method;
						stack[A + 1] = obj;
						pc++;
						break;
					}

					case 21: { // CALL
						const parameterCount = (B === 0) ? (top - A) : (B - 1);
						const func = stack[A];
						if (typeof func !== "function") {
							throw new LuauError(`attempt to call a ${getLuauType(func)} value`, pc);
						}

						const args = stack.slice(A + 1, A + 1 + parameterCount);
						const ret_list = await func(...args);

						// normalize return values
						const retArr = Array.isArray(ret_list) ? ret_list : [ret_list];
						let ret_num = retArr.length;
						if (C === 0) {
							top = A + ret_num - 1;
						} else {
							ret_num = C - 1;
						}
						for (let i = 0; i < ret_num; i++) {
							stack[A + i] = retArr[i];
						};

						break;
					};

					case 22: { // RETURN
						let nresults;
						if ((B - 1) === LUA_MULTRET) {
							nresults = top - A + 1;
						} else {
							nresults = B - 1;
						};

						return stack.slice(A, A + nresults);
					};

					case 23: pc += D; break; // JUMP
					case 24: pc += D; break; // JUMPBACK
					case 25: pc += (stack[A]) ? D : 0; break; // JUMPIF // if (stack[A]) pc += D;
					case 26: pc += (!stack[A]) ? D : 0; break; // JUMPIFNOT // if (!stack[A]) pc += D;
					case 27: pc += (stack[A] === stack[aux]) ? D : 1; break; // JUMPIFEQ
					case 28: pc += (stack[A] <= stack[aux]) ? D : 1; break; // JUMPIFLE
					case 29: pc += (stack[A] < stack[aux]) ? D : 1; break; // JUMPIFLT
					case 30: pc += (stack[A] !== stack[aux]) ? D : 1; break; // JUMPIFNOTEQ
					case 31: pc += (stack[A] > stack[aux]) ? D : 1; break; // JUMPIFNOTLE // <= 1 ? D
					case 32: pc += (stack[A] >= stack[aux]) ? D : 1; break; // JUMPIFNOTLT // < 1 ? D

					case 33: stack[A] = stack[B] + stack[C]; break; // ADD
					case 34: stack[A] = stack[B] - stack[C]; break; // SUB
					case 35: stack[A] = stack[B] * stack[C]; break; // MUL
					case 36: stack[A] = stack[B] / stack[C]; break; // DIV
					case 37: stack[A] = stack[B] % stack[C]; break; // MOD
					case 38: stack[A] = stack[B] ** stack[C]; break; // POW

					case 39: stack[A] = stack[B] + K; break; // ADDK
					case 40: stack[A] = stack[B] - K; break; // SUBK
					case 41: stack[A] = stack[B] * K; break; // MULK
					case 42: stack[A] = stack[B] / K; break; // DIVK
					case 43: stack[A] = stack[B] % K; break; // MODK
					case 44: stack[A] = stack[B] ** K; break; // POWK

					// || false
					case 45: stack[A] = stack[B] && stack[C]; break; // AND // stack[B] ? stack[C] || false : false // stack[A] = value ? (stack[C] || false) : value;
					case 46: stack[A] = stack[B] || stack[C]; break; // OR // value ? value : (stack[C] || false);
					case 47: stack[A] = stack[B] && K; break; // ANDK
					case 48: stack[A] = stack[B] || K; break; // ORK // value ? value : (inst.K || false);


					/*
					// fallback
					let s = stack[B];
					for (let i = B + 1; i <= C; i++) s += stack[i];
					stack[A] = s;
					*/
					case 49: stack[A] = stack.slice(B, C + 1).join(""); break; // CONCAT
					case 50: stack[A] = !stack[B]; break; // NOT
					case 51: stack[A] = -stack[B]; break; // MINUS
					case 52: stack[A] = stack[B].length; break; // LENGTH // ?.length

					case 53: { // NEWTABLE
						stack[A] = Object.create(null); // new Array(aux); (todo)
						pc++;
						break;
					}

					case 54: { // DUPTABLE
						// template = K
						const serialized = Object.create(null); // {}
						for (const ID of Object.values(K)) { // id of K
							serialized[constants[ID]] = undefined; // null
						};
						stack[A] = serialized;

						break;
					}

					case 55: { // SETLIST
						let count = C - 1;
						if (count === LUA_MULTRET) { // (C - 1) === LUA_MULTRET
							count = top - B + 1;
						};

						// table_move(stack, B, B + c - 1, inst.aux, stack[A])
						const dest = stack[A];
						for (let i = 0; i < count; i++) {
							dest[aux + i] = stack[B + i];
						};

						pc++;
						break;
					}

					case 56: { // FORNPREP
						const limit = stack[A] = Number(stack[A]);
						if (Number.isNaN(limit)) throw new LuauError("invalid 'for' limit (number expected)", pc);

						const step = stack[A + 1] = Number(stack[A + 1]);
						if (Number.isNaN(step)) throw new LuauError("invalid 'for' step (number expected)", pc);

						const index = stack[A + 2] = Number(stack[A + 2]);
						if (Number.isNaN(index)) throw new LuauError("invalid 'for' index (number expected)", pc);

						if (step > 0) {
							if (index > limit) pc += D;
						} else if (limit > index) pc += D;

						/*
						if (Math.sign(step) * (index - limit) > 0) pc += D;

						if (step > 0) {
							if (!(index <= limit)) pc += inst.D;
						} else {
							if (!(limit <= index)) pc += inst.D;
						}
						*/

						break;
					}

					case 57: { // FORNLOOP
						const limit = stack[A];
						const step = stack[A + 1];
						const index = stack[A + 2] += step;
						/*
						const index = stack[A + 2] + step;
						stack[A + 2] = index;
						*/

						if (step > 0) {
							if (index <= limit) pc += D;
						} else if (limit <= index) pc += D;

						break;
					}

					case 58: { // FORGLOOP
						// res = K
						top = A + 6;

						const it = stack[A];

						if (typeof it === "function") { // luau_settings.generalizedIteration === false 
							// Traditional iterator
							let vals = await it(stack[A + 1], stack[A + 2]);

							// Normalize iterator return:
							// - undefined/null  -> loop termination
							// - non-array value  -> treat as single value
							// - array            -> multi-return as-is
							if (vals == null) {
								for (let i = 0; i < K; i++) stack[A + 3 + i] = undefined;
								pc++; // exit loop
							} else {
								if (!Array.isArray(vals)) vals = [vals];

								for (let i = 0; i < K; i++) {
									stack[A + 3 + i] = vals[i];
								}

								if (stack[A + 3] != null) {
									stack[A + 2] = stack[A + 3];
									pc += D;
								} else {
									pc++;
								};
							};
						} else {
							const gen =
								generalized_iterators.get(inst) ??
								generalized_iterators.set(inst, (function* () {
									if (typeof it === "object") {
										for (const entry of Object.entries(it)) {
											yield entry;
										}
									}
								})()).get(inst); // default
							if (!gen) throw new TypeError("invalid iterator for forgloop");

							const { value, done } = await gen.next();

							if (!done && value != null) {
								let out = value;
								if (!Array.isArray(out)) out = [out];

								for (let i = 0; i < K; i++) stack[A + 3 + i] = out[i];
								stack[A + 2] = stack[A + 3];
								pc += D;
							} else {
								pc++; // exit loop
							}
						};

						break;
					}

					case 59: { // FORGPREP_INEXT
						if (typeof stack[A] !== "function") {
							throw new TypeError(`attempt to iterate over a ${getLuauType(stack[A])} value`);
						};

						pc += D;
						break;
					}

					case 60: pc++; break; // FASTCALL3

					case 61: { // FORGPREP_NEXT
						if (typeof stack[A] !== "function") {
							throw new TypeError(`attempt to iterate over a ${getLuauType(stack[A])} value`);
						};

						pc += D;
						break;
					}

					case 63: { // GETVARARGS
						let count = B - 1;
						if (count === LUA_MULTRET) { // (B - 1) === LUA_MULTRET
							count = varargs.length; // varargs.len;
							top = A + count - 1;
						};

						for (let i = 0; i < count; i++) {
							stack[A + i] = varargs[i]; // varargs.list[i]
						};

						break;
					}

					case 64: { // DUPCLOSURE
						const { Closure: originalClosure, Index: protoIndex, Upvalues: originalUpvalues } = K;
						const newPrototype = protoList[protoIndex];
						const { nups } = newPrototype;

						const temporaryUpvalues = getUpvalues(nups, true);

						let reused = false;
						if (originalClosure && Array.isArray(originalUpvalues) && originalUpvalues.length === nups) {
							let same = true;
							for (let i = 0; i < nups; i++) {
								const ou = originalUpvalues[i];
								const tu = temporaryUpvalues[i];

								const ov = (ou && ou.store) ? ou.store[ou.index] : undefined;
								const tv = (tu && tu.store) ? tu.store[tu.index] : undefined;

								if (ov !== tv) {
									same = false;
									break;
								}
							}

							if (same) {
								stack[A] = originalClosure;
								reused = true;
							}
						}

						if (!reused) {
							K.Upvalues = temporaryUpvalues;
							K.Closure = engine.newclosure(module, newPrototype, temporaryUpvalues);
							stack[A] = K.Closure;
						}

						break;
					}

					case 65: break; // PREPVARARGS // no-op?

					case 66: { // LOADKX
						stack[A] = K;
						pc++;
						break;
					}

					case 67: pc += E; break; // JUMPX

					/*
					inst.E ||= 0;
					inst.E++;
					*/
					case 69: inst.E = (E || 0) + 1; break; // COVERAGE
					case 70: throw new Error("encountered unhandled CAPTURE"); // CAPTURE

					case 71: stack[A] = K - stack[C]; break; // SUBRK
					case 72: stack[A] = K / stack[C]; break; // DIVRK

					case 73: break; // FASTCALL1
					case 74: pc++; break; // FASTCALL2
					case 75: pc++; break; // FASTCALL2K

					case 76: { // FORGPREP // luau_settings.generalizedIteration
						const iterator = stack[A];
						let gen;
						if (typeof iterator === "object") {
							gen = await Object.entries(iterator)[Symbol.iterator]();
						} else if (typeof iterator?.next === "function") { // assume it is a generator function
							gen = iterator;
						} else {
							throw new TypeError("Unsupported generalized iterator: must be generator or iterable")
						}

						generalized_iterators.set(inst, gen);

						pc += D;
						break;
					}

					case 77: pc += ((stack[A] == null) !== KN) ? D : 1; break; // JUMPXEQKNIL

					case 78: { // JUMPXEQKB
						const ra = stack[A];
						pc += ((typeof ra === "boolean" && ra === K) !== KN) ? D : 1;
						break;
					}

					case 79: pc += ((stack[A] === K) !== KN) ? D : 1; break; // JUMPXEQKN
					case 80: pc += ((stack[A] === K) !== KN) ? D : 1; break; // JUMPXEQKS

					case 81: stack[A] = Math.floor(stack[B] / stack[C]); break; // IDIV
					case 82: stack[A] = Math.floor(stack[B] / K); break; // IDIVK

					default: throw new TypeError("Unsupported Opcode: " + opname + " op: " + op);
				}
			} // end while

			// never reached
			// close open upvalues gracefully
			for (const [i, uv] of open_upvalues) {
				uv.value = uv.store[uv.index];
				uv.store = uv;
				uv.index = "value";
				open_upvalues.delete(i);
			}
		}
	}
}
6 Likes

I can see this being a valuable resource for me. I’ve previously modified the Lua compiler to include a def reserved word (defined boolean to check if something is not nil but will evaluate to true even if the value is false) and allowing for quickly checking the key index inside of a table (print('hello' in {'chomp','lol','hello','silly'}) evaluates to 3). It was a long and very arduous task that likely would have been easier with a guide like this. Thanks for the in-depth bytecode instructions.

1 Like

Thanks for feedback.
Godspeed with learning bytecode compiler :saluting_face:

2 Likes
  • ignoring everything I have posted in the past
  • don’t care, people can choose what they do, even then, I have moved updates to GitHub, the binary isn’t packed, and even then now comes with symbols, anyone with three braincells can check the binary.

I don’t see the point of the post you made other than ragebaiting on a post that’s almost a week old lol

5 Likes

Literally gatekeeping lil bro :skull: :folded_hands:

3 Likes

Bold of you to say he’s gatekeeping while he made the last 2 iterations of the tool open-source… lol!

7 Likes

I seriously don’t understand your excessive ego or your need to try and call everyone a gate keeper.

Even if I were to open source it, you’d not understand anything in it. It won’t help you at all. I already open sourced three iterations, all of which ended up helping the wrong group in general, so if your smartest argument is “gatekeeping” “:skull: :folded_hands:”, then stop responding to my posts, because you’re only making a ridicule of yourself by talking and not knowing to who you’re talking and what they’ve done on the past.

Cheers.

15 Likes

This post would be a lot more useful if it explained how the VM works in full, and not just rehash the Bytecode.h file.

The VM is both register and stack based, registers are enclosed within an executing function, while the stack is used to keep track of the register state of whoevers calling a function (aka, basically every computer).

Each bytecode file contains an anonymous variadic entry function, which the VM uses as an entry point for that script. This usually appears at the bottom of a bytecode blob (but not always).

The VM pushes back so many registers for the given prototype, defined as maxstacksize (silly name!!), then sets the program counter to 0 for that prototype.[1].

When the VM hits CALL or NAMECALL, everything in the allocated registers is put onto the stack, then the VM allocates new registers for the child closure[2]. When that closure returns, the register state is cleared, and the stack is popped to restore the previous stack state.

remarks * [1] Program counter is a register but not in the typical register area, it still gets moved around on the stack. * [2] This only happens with closures from Luau, C funcs dont *usually* manipulate the stack
15 Likes

really incredible resource, we need more of this stuff, i hope you write more in the future :smiley:

1 Like

Thanks. Have been writing (for about ~2 days now) tutorial about animating and rigging for animating on roblox through blender cause turns out nobody made any rigging tutorials yet :frowning_face_with_open_mouth: and all have been using pre-made rigs which i think is kinds lazy.
So yeah more cool tutorials coming dw :smiling_face_with_sunglasses: althrough will be more rare since im developing own game rn.
OPTIMIZE OPTIMIZE OPTIMIZE OPTIMIZE :flexed_biceps:

2 Likes

Yo Yarik W post, I actually learned a lot. Thank you for sharing knowledge with the community, and don’t take the harsh criticism to personally of people who are trying to make dev forum into a non beginner friendly enviorment like stack overflow :skull:

The emojis are also a nice touch, I find them helpfull to make the post less dull and souless.

Keep spreading and sharing your posts, they mad interesting to read.

Btw sorry for bumping

2 Likes

Zhank’s, comrade. Very gud.
The New Luau Order is an equal opportunity for all citizens :saluting_face:

2 Likes

Thank you for reporting a spike in edgy metrics™. Your input has been logged for review. Additional data is welcome if available.

1 Like

Hey, if you still don’t understand bytes, I made a post that hopefully fills that gap in knowledge:

Hey, good luck! Out of curiosity, I wanted to check out this post and see the great experience of our esteemed friend here. Because of the post, I notice the same pattern, and I don’t mean to be annoying, I’m not the only one who seems to share this opinion about Yarik. It’s nothing personal, but Dottik has a point: YOU CAN’T CALL ASSUMERISTS STUPID. My friend, your posts are often aggressive, attacking to gain a foothold. Again, the same pattern makes me realize why you’re so rude to me, Yarik. I really don’t understand why this guy is still allowed to make new posts with his messages. If Roblox is aware of his writing style and his tendency to argue and make offensive posts, perhaps they haven’t reported him yet… but I see that some people have noticed your aggressive way of inflating your ego so everyone thinks you’re the best OP and the best programmer. So, if you don’t have a friend, you really need to try to unite everyone so they can bring new things together.

2 Likes

Not to be rude, but… I don’t really like people who say one thing here but do another elsewhere. So what? Dude, I feel like you’re destroying yourself with your ego. I hope you realize it. You’re the first one to assume and accuse others of using AI, when here you say not to be accused of using AI.

7 Likes