changelog
Every release of the decompiler, newest first. Each is named by its version, the one the API sends with every result, so you can look up what changed in the version that decompiled your file.
activity
fewer placeholders, plainer markers and new readability switches
- Far fewer functions come back as placeholders. Code where both arms of an
ifend in the same place,continuein arepeatwith anuntiltest,pcall(fn, ...)inside a loop that returns, and returns from deep inside loops now decompile. - Fixed: a
repeatreturning a power could print-0where the original printed0. - Fixed:
return x or (if a then b else c)inside arepeatcould read a name that was never defined. - Fixed: an
elseifcould testfalsebecause a temporary reused a parameter's slot. - Fixed: some locals and captured values were read as globals, in
repeat ... untiltests, unused functions and closures. - Fixed: in functions with more than 255 constants, some divisions and
ors used the wrong constant. - Fixed: some closures the original shared were created twice, and
inline-callscould inline a helper that was only set on one path, callingnil. - Markers are shorter and plainer: one banner at the top, a
fix:line where there's a fix, and no shouting. - New switch
const-locals: a local that's never assigned again prints asconst(Luau 0.727 or later parses it). - New switch
field-functions: a table of keyed functions prints aslocal t = {}andfunction t.k(). - New switch
line-notes: under the readable layout, each function's first line ends in-- line N, where it was defined. - New switch
mutable-globals:game.Playersandworkspace.CurrentCameraprint without the parentheses. for _, v in keys, nil, nil doprints asfor _, v in keys do.- Lines match the original more often, including in files with no final newline.
- More
.rbxl/.rbxmfiles open: scripts whose bytecode is stored as text, and.rbxlx/.rbxmxfiles with a stray]]>in a script, which used to be refused whole. modelNamesnow names large scripts too.- Memberships come in three tiers, Pro, Max and Ultra, each with its own daily decompile time. Members get up to 300 seconds a decompile, and a key works from 2 networks at once and 5 a day.
- The Discord bot decompiles with
/decompile, with the same free limit, and/statusshows how many you have left. - The account page shows today's usage as a bar. Memberships are bought in our Discord server.
- The docs are shorter and plainer.
xml place files
- You can now drop
.rbxlxand.rbxmxfiles on the site, just like.rbxland.rbxm. - Fewer functions are left as stubs: many scripts that had a refused function now decompile in full.
- Optimised scripts decompile more often, especially ones that call small helper functions.
- Loops that always return, or can never run, no longer stop the code around them from decompiling.
- Fewer spots are marked as uncertain.
more scripts decompile, and new tools for agents
- More code now decompiles fully instead of being left as a placeholder. This includes
if/elseexpressions, tables built from function calls ({ f(), k = g() }),return f()inside a loop,if a or f() then, early returns inside nestedifs,breakinside nestedrepeatloops, and closures that use a local changed later. - Fixed: some
forloops in scripts with debug info came back as a placeholder that threw an error the original never threw. - Fixed: in some
-O2scripts with debug info, a variable used in arepeat ... untilcondition was read as a global instead. - Fixed: some
-O2scripts with debug info failed to decompile at all. - More
forloops in scripts with debug info now print as loops instead of placeholders. function f() end, wherefis already a local, no longer creates a second local namedf.- Lines match the original more often in scripts with debug info.
- Tables of tables now print one row per line.
- Agents get new tools:
search_sourceto search the output,compareto diff two results function by function, anddecompile_manyfor up to 16 scripts at once.get_functionfinds a function by name and shows what it calls and what calls it. Small scripts (120 lines or fewer) come back with their source right away. - MCP clients now ask before running
decompile, since each call uses one of your free decompiles. /v1/decompilenow also tells you where each function starts and ends, which functions call which, and which function each note belongs to.
place files without false time-outs
- A script that only waited in line behind others is no longer refused as having run past its time limit: it is answered "busy" and the site tries it again.
maxSecondsnow counts from when the decompiler starts on a job. - Model names no longer hold a decompile open while the names are chosen, so large
.rbxmand.rbxlfiles run with fewer refusals. - Fewer "overloaded" errors when many jobs run at once: a retry now waits a moment before it goes again.
- Free decompiles no longer run out early in the day.
- Members may send files up to 8 MiB, and get 4 hours of decompile time a day, which now includes the naming model's time. Big place files start faster for members.
- Fixed at
-O0and-O1: a local whose only later write was never read printed inline, which could let a weak table drop its key. - Fixed at
-O2with full debug info: a function whose calls were inlined could come back with more locals than the compiler accepts. - With full debug info, output keeps more of the original:
t[8]stayst[8]when a constant local happens to hold 8, an inlined call's parameters print as the caller's locals (local a = t[1]), a numericforkeeps its index name when its start is an upvalue, and a one-line loop keeps itsendon that line. - Older bytecode (version 2): a generic
forover an iterator the decompiler can't prove now decompiles instead of standing in as a stub. - At
-O0, aforthat returns from inside awhileunderrepeat ... until trueprints as a plain loop instead of a marked one. - At
-O2, a chain of more than five inlined functions decompiles to the same program.
2048 decompiles at once
- Members can now really run up to 2048 decompiles at once. A big run, such as a whole place file, is spread over several connections, so it finishes in well under a minute instead of stalling.
- The site and the client for executors open more connections as a run grows, and close them again once it's done.
- A decompile no longer fails because a heavy one was running next to it. When that happens, it's tried again before you see an error.
- Model names no longer hold up other decompiles: names are added after the decompile itself has finished.
- The place file panel no longer shows a "waiting" countdown or a "starting the naming model" message.
accounts, place files and the stream
- Accounts: make one on the new account page, with no email or password. The account's number is how you sign in, so keep it somewhere safe. On the same page you make and revoke keys for your programs, which send them as
Authorization: Bearer ugk_…. See keys and accounts. - A member's decompiles don't count against the free tier. Up to 2048 run at once, with no limit a minute, and each account gets an hour of decompile time a day. Every response says when the membership ends, in
Member-Until. To get a membership for now, mail support@unreadable.gg. - Place and model files: drop a
.rbxlor.rbxmon the site. It opens in your browser, only the scripts' bytecode is sent, and you get the file back with every script's source filled in. Sent to the API, a place file gets422 container. - Many decompiles over one connection: the stream is a WebSocket you send jobs on, with a client for executors.
- Older files: the decompiler reads bytecode versions 1 and 2, early pre-release files and RSB1-wrapped files. These can't recompile to the same bytes, so the output aims for the same program.
- Free tier: 16 decompiles at once (was 2), 120 a minute (was 60) and 5 minutes of decompile time a day. The new
maxSecondsoption (1 to 30) makes a decompile give up sooner. A decompile that times out now uses a free decompile. See limits. - The API reads no query strings at all: options and fields go in a JSON body, and any query string gets
400 unknown-field. - Closing a request while it decompiles now stops the decompile.
- The viewer: the bytecode pane is gone, so the source takes the full width. The presets sit on the bar, and the switches open in a drawer beside the code. On the home page, "options" sets them before you drop a file, and when you're signed in your choice is kept for next time.
- The docs are now five short pages: an overview, the API, readability, the stream and agents. Old links still land in the right place.
- Model names arrive sooner, with no wait for the model to start. Output over 200,000 characters keeps its default names. A function stored on a table that uses the table as
selfprints asfunction T:m(). - Fixed a number of cases where the decompiled code didn't behave like the original, mostly in optimized files: a loop could read a value after it had already changed, an argument could come back as
nil, functions meant to be made fresh each time came out shared, and some calls ran in a different order. - Fewer functions come out as
error(…)stubs, especially in optimized files, loops that skip ahead withcontinue, longand/orconditions and very large modules. -g2keeps more of your names: loop variables, declaration lists, swaps and the names around inlined calls.for _, bno longer prints asfor _1, b.-g1keeps each statement on its recorded line, and-t1brings back the types of loop variables and upvalues.@nativeis printed on each function that had it, and a local namedgotokeeps its name.- Fixed hangs: a module that never finished at
-O2, deeply nestedifexpressions, and a small place file that could use gigabytes of memory. - Large files decompile faster and use less memory.
readability presets and model names
- Choose how the output reads:
faithful(the default, unchanged),balancedorreadable, or turn single switches on and off, in the viewer or through the API. A switch changes only how the code is written, never what it does. See readability. - The switches: roll an unrolled loop back into
for i = 1, 3 do ... end, return early withif not c then return endinstead of nesting the rest of a function in oneif, writen *= 3forn = n * 3, drop unused values and branches, write16for2 * 8ormath.pi / 2for1.5707963267948966, write a small helper's call as its body, add blank lines between unrelated steps, add the types inference proves, and note on each function's first line which outer variables it captures, like-- upvalues: speed (copy), pos (ref). - Model names: with
balanced,readableor themodelNamesswitch, a naming model names the values your file left unnamed, solocal v3 = arg0.Healthcan readlocal health = player.Health. It never picks a name that could change the program, and if the model is busy or down you get the usual names and a note saying so. - Cleaner default output, with the same meaning: methods print as
function Spring:getTarget()usingself, functions asfunction f()rather thanf = function(), services and modules take their names, as inlocal Players = game:GetService("Players"), non-empty tables print as{ 1, 2 }, guard clauses get a line each, andt.a.b -= eno longer goes through a temporary. - At
-g2, a function that captures outer variables keeps its own name, where before it printed asv1. - Fixed two cases where the output ran differently from the input: a captured local first set by an
if a then b else cexpression, and an unused function inside adoblock at-O0. - The first line now says only how long the decompile took, plus the naming time when names came from the model:
-- unreadable.gg ~ 140ms (+3.2s naming). The setting used comes back assettingin the result. - Agents can use the decompiler through its MCP server, and send a file straight from disk with
POST /v1/handlesrather than passing it through their context. - The API can stream progress: send
Accept: application/x-ndjsonand get one line per stage (decompiling, renaming, done). - The
unverifiedWarningoption is gone. A file whose opcodes are numbered differently from stock Luau gets anunverifiablenote, never a comment at the top of the source. - Large files decompile faster and use less memory.
100 free decompiles a day during the beta
- Beta only: while unreadable.gg is in beta, you get 100 free decompiles a day instead of 5, on the site and through the API together. The limit will change when the beta ends.
- Cleaner
-O2output: functions the compiler inlined, even deeply nested ones, print as calls likef(1) + f(3)again, not as long runs of copied code. - Large files decompile up to 4× faster while using half the memory.
- Hand-edited or malformed files: where the output can't match exactly, it now carries a note instead of looking clean. For example, a function that uses more locals than the compiler allows, or that reads
...without declaring it.
the site and the api are live
- Decompile a file in your browser: drop it on unreadable.gg. The source shows beside your file's bytes, with its verdict, ready to copy or download.
- Scripts and agents can send the same file to
POST https://api.unreadable.gg/v1/decompileand get back the source, a verdict, notes and the version that wrote it. See the API docs. - Every connection gets 5 free decompiles a day, on the site and through the API together, with no key or account. Errors and refused decompiles don't count.
- Your files are never stored or logged: see privacy.
bytecode version 14
- Reads bytecode version 14 and the calls current Luau compilers write by default. Modules from them used to be refused.
- Notes and errors carry fixed codes a program can act on, such as
function-refusedandnot-luau-bytecode: see notes and errors. - A function the compiler kept from code it removed, such as the body of
if false then … end, is printed back insideif false then … endinstead of being left out. Where it has to guess what the function captured, the guess is marked. - Fixed a case where the first statements of a loop could be left out of the output.
- The parameters of a function that also takes
...keep their names, and so do locals that are copied and read later. local a, b = -0.5, #xprints as one list, as it was written.- Very large modules decompile much faster at
-O0and-O1. - Three more shapes are refused rather than guessed: a branch whose inner arm computes a value, a table constructor whose parts don't agree, and a function that needs too many passes to settle.
continue in nested loops
- A
continueinside nested loops comes back ascontinue. - An assignment to several places at once, such as
a.x, b.y = 1, 2, comes back as one statement.
fewer wrong programs
- Fixed several cases where the output could have run differently from the input: assignments to undeclared and captured variables, and generic
forloops left by areturn. Where one can't be printed faithfully yet, it's refused instead. - A module that had started taking seconds to decompile is back to a few hundred milliseconds.
- The decompiler builds for WebAssembly, which is how the site and the API run it.
conditions as one expression
- Conditions with three or more tests print as one
and/orexpression. - A variable a loop carries from one pass to the next is no longer declared again on every pass.
and/or as values
and/orchains used as values fold back into one expression, as inlocal x = a and b or c.- A damaged or crafted file can no longer make the decompiler reserve memory for more than the file holds: every count it reads is checked first.
table constructors and recorded types
- Table constructors come back as
{ … }instead of a table filled in one assignment at a time. - A type the bytecode records for a local is printed with it, as in
local w: number. if notkeeps its two arms in the order they were written, and ado … endblock keeps its locals inside it.- A statement that starts with
(gets a;before it, so it isn't read as a call on the line above. - A file from another Lua version is refused by name instead of read as Luau. See Lua versions.
first version
- Decompiles Luau bytecode back into source, and marks or refuses what it can't reproduce instead of guessing.
- Reading a file can't crash the decompiler: a crafted file that would overflow its stack is turned away, and nothing on the way in may abort.