decompile.js
JavaScript source for the SWF disassembler, 1260 lines: running the instructions symbolically, rebuilding loops, branches and switches from the shapes the compiler leaves, and printing the result as ActionScript. It's lib/decompile.js in swf-disassembler.zip.
// Turns decoded AVM1 actions back into ActionScript.
//
// AVM1 is a stack machine, so the decompiler runs the code symbolically: pushes put expressions on a
// stack, operators combine them, and actions with side effects become statements. Control flow is
// rebuilt from the shapes the Flash compiler produces for if/else, while, do/while, for, for..in, &&,
// || and ?:. Anything that doesn't match a known shape is left as labels and gotos, so nothing is lost.
import { registerNames } from './actions.js';
const NOT = 0x12, POP = 0x17, IF = 0x9D, JUMP = 0x99, PUSH = 0x96, STORE = 0x87, EQUALS2 = 0x49, PLAY = 0x06;
const PROPERTIES = ['_x', '_y', '_xscale', '_yscale', '_currentframe', '_totalframes', '_alpha', '_visible',
'_width', '_height', '_rotation', '_target', '_framesloaded', '_name', '_droptarget', '_url', '_highquality',
'_focusrect', '_soundbuftime', '_quality', '_xmouse', '_ymouse'];
// Binary operators: opcode -> [operator, precedence]
const BINARY = {
0x0A: ['+', 11], 0x0B: ['-', 11], 0x0C: ['*', 12], 0x0D: ['/', 12], 0x0E: ['==', 8], 0x0F: ['<', 9],
0x10: ['and', 4], 0x11: ['or', 3], 0x13: ['eq', 8], 0x21: ['add', 11], 0x29: ['lt', 9],
0x3F: ['%', 12], 0x47: ['+', 11], 0x48: ['<', 9], 0x49: ['==', 8], 0x54: ['instanceof', 9],
0x60: ['&', 7], 0x61: ['|', 5], 0x62: ['^', 6], 0x63: ['<<', 10], 0x64: ['>>', 10], 0x65: ['>>>', 10],
0x66: ['===', 8], 0x67: ['>', 9], 0x68: ['gt', 9],
};
const PRECEDENCE = {
'||': 3, 'or': 3, '&&': 4, 'and': 4, '|': 5, '^': 6, '&': 7, '==': 8, '!=': 8, '===': 8, '!==': 8, 'eq': 8, 'ne': 8,
'<': 9, '>': 9, '<=': 9, '>=': 9, 'lt': 9, 'gt': 9, 'le': 9, 'ge': 9, 'instanceof': 9,
'<<': 10, '>>': 10, '>>>': 10, '+': 11, '-': 11, 'add': 11, '*': 12, '/': 12, '%': 12,
};
// One-operand actions that are really built-in functions
const BUILTIN_1 = {
0x14: 'length', 0x18: 'int', 0x30: 'random', 0x31: 'mblength', 0x32: 'ord', 0x33: 'chr', 0x36: 'mbord',
0x37: 'mbchr', 0x45: 'targetPath', 0x4A: 'Number', 0x4B: 'String', 0x25: 'removeMovieClip', 0x26: 'trace',
};
// Thrown when a range that should be a pure expression turns out to need statements
class NotAnExpression extends Error {}
// Decompiles a list of actions into statements. names maps registers to the names they hold.
export function decompile(list, options = {}) {
const ctx = makeContext(list, options.names || new Map(), new Set(options.declared || []));
ctx.fn2 = !!options.fn2;
let result = block(ctx, 0, list.length, [], null, false);
// If gotos were needed, run again with labels at their targets
if (ctx.gotos.size) {
ctx.labels = new Set(ctx.gotos);
ctx.placed = new Set();
ctx.declared = new Set(options.declared || []);
result = block(ctx, 0, list.length, [], null, false);
}
return result.stmts;
}
function makeContext(list, names, declared) {
const index = new Map();
list.forEach((ins, i) => index.set(ins.offset, i));
const last = list[list.length - 1];
const endOffset = last ? last.offset + last.size : 0;
index.set(endOffset, list.length);
// Every backward branch marks a loop: its target is the loop's first instruction
const backEdges = new Map();
list.forEach((ins, i) => {
if ((ins.code === JUMP || ins.code === IF) && ins.target <= ins.offset && index.has(ins.target)) {
const h = index.get(ins.target);
if (!backEdges.has(h) || backEdges.get(h) < i)
backEdges.set(h, i);
}
});
return { list, index, endOffset, backEdges, names, declared, labels: new Set(), placed: new Set(), gotos: new Set(), active: new Set() };
}
// ---- Expression nodes --------------------------------------------------------------------------------
const lit = (v) => ({ k: 'lit', v });
const variable = (name) => ({ k: 'var', name });
const member = (obj, prop) => ({ k: 'mem', obj, prop });
const call = (fn, args) => ({ k: 'call', fn, args });
const binary = (op, a, b) => ({ k: 'bin', op, a, b });
const unary = (op, a) => ({ k: 'un', op, a });
function not(e) {
return unary('!', e);
}
// The opposite of a condition, written the way the source most likely had it
function negate(e) {
if (e.k === 'un' && e.op === '!')
return simplifyCondition(e.a);
return simplifyCondition(not(e));
}
function simplifyCondition(e) {
if (e.k === 'logic')
return { ...e, a: simplifyCondition(e.a), b: simplifyCondition(e.b) };
if (e.k !== 'un' || e.op !== '!')
return e;
const a = e.a;
if (a.k === 'lit' && typeof a.v === 'boolean')
return lit(!a.v);
if (a.k === 'un' && a.op === '!')
return simplifyCondition(a.a);
if (a.k === 'bin') {
const flip = { '==': '!=', '!=': '==', '===': '!==', '!==': '===', '<': '>=', '>': '<=', '<=': '>', '>=': '<', 'eq': 'ne', 'lt': 'ge', 'gt': 'le' };
if (flip[a.op])
return binary(flip[a.op], a.a, a.b);
}
return e;
}
// A string pushed as a name becomes an identifier
function nameOf(e) {
return e.k === 'lit' && typeof e.v === 'string' ? variable(e.v) : { k: 'eval', e };
}
function numberOf(e) {
return e.k === 'lit' && typeof e.v === 'number' ? e.v : null;
}
// ---- The decompiler ----------------------------------------------------------------------------------
// Decompiles list[lo..hi). stack holds values left by code before this range. loop is the innermost
// loop around it (where break and continue go). In expression mode, any statement is a failure.
function block(ctx, lo, hi, stack, loop, exprMode) {
const { list, index } = ctx;
const out = [];
const off = (i) => (i < list.length ? list[i].offset : ctx.endOffset);
const idx = (offset) => index.get(offset);
const emit = (s) => {
if (exprMode)
throw new NotAnExpression();
out.push(s);
};
const pop = () => (stack.length ? stack.pop() : { k: 'missing' });
const push = (e) => stack.push(e);
const popArgs = (n) => {
const args = [];
for (let j = 0; j < n; j++)
args.push(pop());
return args;
};
const tryExpr = (a, b) => {
try {
const r = block(ctx, a, b, [], loop, true);
return r.stack.length === 1 ? r.stack[0] : null;
} catch (e) {
if (e instanceof NotAnExpression)
return null;
throw e;
}
};
const goTo = (target) => {
ctx.gotos.add(target);
return { k: 'goto', target };
};
const jumpStatement = (target) => {
if (loop && target === loop.brk)
return { k: 'break' };
if (loop && target === loop.cont)
return { k: 'continue' };
if (target === ctx.endOffset)
return { k: 'return' };
return goTo(target);
};
// a = b = c compiles as: c, storeRegister 0, (set b), push r:0, (set a). The first set becomes an
// assignment expression, which the push of r:0 then stands for.
let chainPending = false;
let chainValue = null;
const chained = (target, value) => {
if (!chainPending)
return false;
chainPending = false;
chainValue = { k: 'assignx', target, value };
return true;
};
// a[i++] compiles as: a, i (the old value, left on the stack), then i = i + 1, then the set that
// uses the old value. If x = x ± 1 happens while an x is still waiting on the stack, that x is x++.
const postfix = (target, value) => {
if (value.k !== 'bin' || (value.op !== '+' && value.op !== '-') || !same(value.a, target) ||
value.b.k !== 'lit' || value.b.v !== 1)
return false;
for (let n = stack.length - 1; n >= 0; n--)
if (same(stack[n], target)) {
stack[n] = { k: 'postfix', op: value.op + value.op, target };
return true;
}
return false;
};
let i = lo;
while (i < hi) {
const ins = list[i];
if (ctx.labels.has(ins.offset) && !ctx.placed.has(ins.offset) && !exprMode) {
ctx.placed.add(ins.offset);
out.push({ k: 'label', target: ins.offset });
}
// ---- Loops: this instruction is the target of a later backward branch inside this range
const j = ctx.backEdges.get(i);
if (j !== undefined && j < hi && !ctx.active.has(i) && !exprMode) {
ctx.active.add(i);
const exit = off(j + 1);
const inner = { brk: exit, cont: off(i) };
const back = list[j];
let next = j + 1;
const forIn = stack.length && stack[stack.length - 1].k === 'enum' ? forInShape(list, i, j, off) : null;
if (back.code === IF && j === i + 3 && list[i].code === PUSH && list[i].values.length === 1 &&
list[i].values[0].type === 'null' && list[i + 1].code === EQUALS2 && list[i + 2].code === NOT) {
// push null, equals2, not, if (back): pops what a for..in left on the stack before a break
// or return leaves the loop. It isn't in the source.
} else if (back.code === IF) {
// do { body } while (cond): the condition is left on the stack at the backward branch
const r = block(ctx, i, j, [], inner, false);
const cond = r.stack.length ? r.stack.pop() : lit(true);
emit({ k: 'dowhile', body: r.stmts, cond });
} else if (forIn) {
// for (x in obj): enumerate pushed a null and then each name, and the loop pops them in turn
const obj = pop().obj;
const r = block(ctx, i + 4, j, [], { brk: forIn.brk, cont: off(i) }, false);
next = forIn.next;
const body = r.stmts;
let target = { k: 'var', name: '?' };
let declare = false;
if (body.length && body[0].k === 'assign' && body[0].value.k === 'reg' && body[0].value.n === 0) {
target = body[0].target;
declare = body[0].declare;
body.shift();
}
emit({ k: 'forin', target, declare, obj, body });
} else {
// while (cond) { body }: the condition is tested at the top, and exits past the backward jump
let k = -1;
for (let m = i; m < j; m++)
if (list[m].code === IF && list[m].target === exit) { k = m; break; }
const cond = k >= 0 ? tryExpr(i, k) : null;
if (cond) {
const r = block(ctx, k + 1, j, [], inner, false);
emit(forLoop(out, negate(cond), r.stmts));
} else {
const r = block(ctx, i, j, [], inner, false);
emit({ k: 'while', cond: lit(true), body: r.stmts });
}
}
ctx.active.delete(i);
i = next;
continue;
}
switch (ins.code) {
case 0x00: // end
case 0x88: // constantPool
break;
case PUSH:
ins.values.forEach((v, n) => {
if (n === 0 && chainValue && v.type === 'register' && v.value === 0) {
push(chainValue);
chainValue = null;
} else {
push(v.type === 'register' ? reg(ctx, v.value) : lit(v.value));
}
});
break;
case POP: {
const e = pop();
if (e.k !== 'missing' && e.k !== 'enum' && hasEffect(e))
emit({ k: 'expr', e });
break;
}
case 0x4C: { // pushDuplicate: the start of a && or || if a branch and a pop follow
let k = i + 1;
let and = false;
if (list[k] && list[k].code === NOT) {
and = true;
k++;
}
if (list[k] && list[k].code === IF && list[k].target > list[k].offset && list[k + 1] && list[k + 1].code === POP) {
const end = idx(list[k].target);
if (end !== undefined && end <= hi) {
const right = tryExpr(k + 2, end);
if (right) {
push({ k: 'logic', op: and ? '&&' : '||', a: pop(), b: right });
i = end;
continue;
}
}
}
const top = pop();
push(top);
push(top);
break;
}
case 0x4D: { // stackSwap
const a = pop(), b = pop();
push(a);
push(b);
break;
}
case 0x1C: // getVariable
push(nameOf(pop()));
break;
case 0x1D: { // setVariable
const value = pop();
const target = nameOf(pop());
if (!chained(target, value) && !postfix(target, value))
emit({ k: 'assign', target, value });
break;
}
case 0x3C: { // defineLocal
const value = pop();
emit({ k: 'assign', target: nameOf(pop()), value, declare: true });
break;
}
case 0x41: // defineLocal2
emit({ k: 'declare', target: nameOf(pop()) });
break;
case 0x4E: { // getMember
const prop = pop();
push(member(pop(), prop));
break;
}
case 0x4F: { // setMember
const value = pop();
const prop = pop();
const target = member(pop(), prop);
if (!chained(target, value) && !postfix(target, value))
emit({ k: 'assign', target, value });
break;
}
case STORE: { // storeRegister: copies the top of the stack into a register, leaving it there
if (!exprMode) {
const sw = switchAt(ctx, i, hi, stack[stack.length - 1], loop);
if (sw) {
pop();
emit(sw.stmt);
i = sw.next;
continue;
}
}
// The start of a chained assignment: leave the value where it is for the set that follows
const nextSet = list[i + 1] && (list[i + 1].code === 0x4F || list[i + 1].code === 0x1D);
const thenR0 = list[i + 2] && list[i + 2].code === PUSH && list[i + 2].values[0] &&
list[i + 2].values[0].type === 'register' && list[i + 2].values[0].value === 0;
if (ins.register === 0 && nextSet && thenR0 && i + 2 < hi) {
chainPending = true;
break;
}
const value = pop();
const r = reg(ctx, ins.register);
if (exprMode) {
push({ k: 'assignx', target: r, value });
break;
}
const popped = list[i + 1] && list[i + 1].code === POP && i + 1 < hi;
if (popped && postfix(r, value)) {
i += 2;
continue;
}
const declare = ctx.fn2 && !ctx.declared.has(ins.register);
if (declare)
ctx.declared.add(ins.register);
emit({ k: 'assign', target: r, value, declare });
if (popped) {
i += 2;
continue;
}
push(r);
break;
}
case 0x3D: { // callFunction
const fn = nameOf(pop());
const n = numberOf(pop()) ?? 0;
push(call(fn, popArgs(n)));
break;
}
case 0x52: // callMethod
case 0x53: { // newMethod
const name = pop();
const obj = pop();
const n = numberOf(pop()) ?? 0;
const args = popArgs(n);
const fn = name.k === 'lit' && (name.v === undefined || name.v === '') ? obj : member(obj, name);
push(ins.code === 0x52 ? call(fn, args) : { k: 'new', fn, args });
break;
}
case 0x40: { // newObject
const fn = nameOf(pop());
const n = numberOf(pop()) ?? 0;
push({ k: 'new', fn, args: popArgs(n) });
break;
}
case 0x42: { // initArray
const n = numberOf(pop()) ?? 0;
push({ k: 'array', items: popArgs(n) });
break;
}
case 0x43: { // initObject
const n = numberOf(pop()) ?? 0;
const pairs = [];
for (let m = 0; m < n; m++) {
const value = pop();
pairs.unshift([pop(), value]);
}
push({ k: 'object', pairs });
break;
}
case NOT:
push(not(pop()));
break;
case 0x50: // increment
push(binary('+', pop(), lit(1)));
break;
case 0x51: // decrement
push(binary('-', pop(), lit(1)));
break;
case 0x44: // typeOf
push(unary('typeof ', pop()));
break;
case 0x3A: { // delete
const prop = pop();
push(unary('delete ', member(pop(), prop)));
break;
}
case 0x3B: // delete2
push(unary('delete ', nameOf(pop())));
break;
case 0x22: { // getProperty
const prop = numberOf(pop());
const target = pop();
push({ k: 'prop', target, name: PROPERTIES[prop] ?? `property${prop}` });
break;
}
case 0x23: { // setProperty
const value = pop();
const prop = numberOf(pop());
const target = pop();
emit({ k: 'assign', target: { k: 'prop', target, name: PROPERTIES[prop] ?? `property${prop}` }, value });
break;
}
case 0x15: { // stringExtract
const count = pop(), at = pop(), s = pop();
push(call(variable('substring'), [s, at, count]));
break;
}
case 0x35: { // mbStringExtract
const count = pop(), at = pop(), s = pop();
push(call(variable('mbsubstring'), [s, at, count]));
break;
}
case 0x34: // getTime
push(call(variable('getTimer'), []));
break;
case 0x2B: { // castOp
const obj = pop();
push(call(pop(), [obj]));
break;
}
case 0x46: // enumerate
push({ k: 'enum', obj: nameOf(pop()) });
break;
case 0x55: // enumerate2
push({ k: 'enum', obj: pop() });
break;
case 0x3E: // return
emit({ k: 'return', e: pop() });
break;
case 0x2A: // throw
emit({ k: 'throw', e: pop() });
break;
case 0x26: // trace
emit({ k: 'expr', e: call(variable('trace'), [pop()]) });
break;
case 0x69: { // extends
const superclass = pop();
emit({ k: 'extends', sub: pop(), superclass });
break;
}
case 0x2C: { // implementsOp
const ctor = pop();
const n = numberOf(pop()) ?? 0;
emit({ k: 'implements', ctor, interfaces: popArgs(n) });
break;
}
case 0x9B: // defineFunction
case 0x8E: { // defineFunction2
const fn = functionNode(ins, ctx);
if (ins.fname)
emit({ k: 'function', fn });
else
push(fn);
break;
}
case 0x94: { // with
const obj = pop();
emit({ k: 'with', obj, body: decompile(ins.body, { names: ctx.names, declared: ctx.declared }) });
break;
}
case 0x8F: { // try
const opts = { names: ctx.names, declared: ctx.declared };
emit({
k: 'try',
body: decompile(ins.body, opts),
catchName: ins.hasCatch ? (ins.catchInRegister ? regName(ctx, ins.catchRegister) : ins.catchName) : null,
catchBody: ins.hasCatch ? decompile(ins.catchBody, opts) : null,
finallyBody: ins.hasFinally ? decompile(ins.finallyBody, opts) : null,
});
break;
}
// ---- Timeline and movie clip actions
case 0x04: emit(callStatement('nextFrame')); break;
case 0x05: emit(callStatement('prevFrame')); break;
case PLAY: emit(callStatement('play')); break;
case 0x07: emit(callStatement('stop')); break;
case 0x08: emit(callStatement('toggleHighQuality')); break;
case 0x09: emit(callStatement('stopAllSounds')); break;
case 0x28: emit(callStatement('stopDrag')); break;
case 0x81: // gotoFrame: frames count from 0 here and from 1 in ActionScript
if (list[i + 1] && list[i + 1].code === PLAY && i + 1 < hi) {
emit(callStatement('gotoAndPlay', [lit(ins.frame + 1)]));
i += 2;
continue;
}
emit(callStatement('gotoAndStop', [lit(ins.frame + 1)]));
break;
case 0x9F: // gotoFrame2
emit(callStatement(ins.play ? 'gotoAndPlay' : 'gotoAndStop', [pop()]));
break;
case 0x8C: // goToLabel
emit(callStatement('gotoAndStop', [lit(ins.value)]));
break;
case 0x9E: // call
emit(callStatement('call', [pop()]));
break;
case 0x83: // getURL
emit(getURLStatement(ins.url, ins.window));
break;
case 0x9A: { // getURL2
const target = pop();
const url = pop();
const method = ins.method === 'none' ? [] : [lit(ins.method)];
if (ins.loadVariables)
emit(callStatement(ins.loadTarget ? 'loadVariables' : 'loadVariablesNum', [url, target, ...method]));
else if (ins.loadTarget)
emit(callStatement('loadMovie', [url, target, ...method]));
else
emit(callStatement('getURL', [url, target, ...method]));
break;
}
case 0x8B: // setTarget
emit(callStatement('setTarget', [lit(ins.value)]));
break;
case 0x20: // setTarget2
emit(callStatement('setTarget', [pop()]));
break;
case 0x8A: // waitForFrame
emit({ k: 'raw', text: `ifFrameLoaded (${ins.frame + 1}) // skips ${ins.skip} actions if not` });
break;
case 0x8D: // waitForFrame2
emit({ k: 'raw', text: `ifFrameLoaded (${print(pop())}) // skips ${ins.skip} actions if not` });
break;
case 0x27: { // startDrag: Flash 4 pushes the flags as the strings '0' and '1'
const target = pop(), lock = pop(), constrain = pop();
const args = [target, lock];
if (constrain.k !== 'lit' || Number(constrain.v)) {
const y2 = pop(), x2 = pop(), y1 = pop(), x1 = pop();
args.push(x1, y1, x2, y2);
}
emit(callStatement('startDrag', args));
break;
}
case 0x24: { // cloneSprite
const depth = pop(), target = pop(), source = pop();
emit(callStatement('duplicateMovieClip', [source, target, depth]));
break;
}
case 0x2D: // fsCommand2
emit(callStatement('fscommand2', popArgs(numberOf(pop()) ?? 0)));
break;
// ---- Branches
case IF: {
const value = pop();
const T = ins.target;
const t = idx(T);
if (t === undefined || T <= ins.offset) {
emit({ k: 'if', cond: value, then: [goTo(T)], else: null });
break;
}
// value === a, if L1, value === b, if L2, jump L3: a switch from the SWF 6 compiler
if (!exprMode && value.k === 'bin' && value.op === '===') {
const sw = switchChain(ctx, i, hi, value, loop);
if (sw) {
emit(sw.stmt);
i = sw.next;
continue;
}
}
// c ? a : b compiles as c, if L1, b, jump L2, L1: a, L2:
if (t - 1 > i && list[t - 1].code === JUMP && list[t - 1].target > T) {
const e = idx(list[t - 1].target);
if (e !== undefined && e <= hi) {
const ifFalse = tryExpr(i + 1, t - 1);
const ifTrue = ifFalse && tryExpr(t, e);
if (ifFalse && ifTrue) {
push({ k: 'cond', c: value, a: ifTrue, b: ifFalse });
i = e;
continue;
}
}
}
// A branch out of this range: break, continue or goto
if (t > hi) {
emit({ k: 'if', cond: value, then: [jumpStatement(T)], else: null });
break;
}
// if (c) { a } else { b } compiles as c, not, if L1, a, jump L2, L1: b, L2:
if (t - 1 > i && list[t - 1].code === JUMP && list[t - 1].target > T) {
const E = list[t - 1].target;
const e = idx(E);
if (e !== undefined && e <= hi) {
const a = block(ctx, i + 1, t - 1, [], loop, false);
const b = block(ctx, t, e, [], loop, false);
emit({ k: 'if', cond: negate(value), then: a.stmts, else: b.stmts });
i = e;
continue;
}
}
// if (c) { a } compiles as c, not, if L1, a, L1:
const a = block(ctx, i + 1, t, [], loop, false);
emit({ k: 'if', cond: negate(value), then: a.stmts, else: null });
i = t;
continue;
}
case JUMP:
// A jump to the next instruction, or to the end of this block, is the same as carrying on,
// unless it's a break
if (ins.target === off(i + 1) || (ins.target === off(hi) && !(loop && ins.target === loop.brk)))
break;
emit(jumpStatement(ins.target));
break;
default:
if (BINARY[ins.code]) {
const b = pop();
push(binary(BINARY[ins.code][0], pop(), b));
} else if (BUILTIN_1[ins.code]) {
const e = call(variable(BUILTIN_1[ins.code]), [pop()]);
if (ins.code === 0x25 || ins.code === 0x26)
emit({ k: 'expr', e });
else
push(e);
} else {
emit({ k: 'raw', text: `// ${ins.name}` });
}
}
i++;
}
return { stmts: out, stack };
}
// A switch compiles as: the value, storeRegister r, then for each case push (r,) value, strictEquals,
// if (case label), and finally a jump to the default case or past the end. Each break jumps to the end.
function switchAt(ctx, i, hi, disc, loop) {
const { list, index } = ctx;
const STRICT = 0x66;
const r = list[i].register;
const isCase = (k, first) => list[k] && list[k].code === PUSH && list[k + 1] && list[k + 1].code === STRICT &&
list[k + 2] && list[k + 2].code === IF && list[k + 2].target > list[k + 2].offset &&
(first ? list[k].values.length === 1
: list[k].values.length === 2 && list[k].values[0].type === 'register' && list[k].values[0].value === r);
if (!disc || !isCase(i + 1, true))
return null;
const cases = [];
let k = i + 1;
let first = true;
while (isCase(k, first)) {
const v = list[k].values[first ? 0 : 1];
cases.push({ value: v.type === 'register' ? reg(ctx, v.value) : lit(v.value), target: list[k + 2].target });
k += 3;
first = false;
}
return buildSwitch(ctx, cases, k, hi, disc, loop);
}
// The older compiler (SWF 6) evaluates the value again for each case: value === case, if (label).
// This starts at the first if, with its comparison already popped.
function switchChain(ctx, i, hi, first, loop) {
const { list } = ctx;
const disc = first.a;
const cases = [{ value: first.b, target: list[i].target }];
let k = i + 1;
while (k < hi && list[k].code !== JUMP) {
let m = k;
while (m < hi && list[m].code !== IF && list[m].code !== JUMP)
m++;
if (m >= hi || list[m].code !== IF || list[m].target <= list[m].offset)
return null;
let e = null;
try {
const r = block(ctx, k, m, [], loop, true);
e = r.stack.length === 1 ? r.stack[0] : null;
} catch (err) {
if (!(err instanceof NotAnExpression))
throw err;
}
if (!e || e.k !== 'bin' || e.op !== '===' || !same(e.a, disc))
return null;
cases.push({ value: e.b, target: list[m].target });
k = m + 1;
}
return buildSwitch(ctx, cases, k, hi, disc, loop);
}
// The case bodies follow the jump at index k, which goes to the default case or past the end
function buildSwitch(ctx, cases, k, hi, disc, loop) {
const { list, index } = ctx;
if (!list[k] || list[k].code !== JUMP || list[k].target <= list[k].offset)
return null;
const D = list[k].target;
const bodyStart = k + 1;
const off = (n) => (n < list.length ? list[n].offset : ctx.endOffset);
const limit = off(hi);
// The end is where the breaks go: the furthest forward jump that stays inside this range
const starts = [...cases.map((c) => c.target), D];
const lastStart = Math.max(...starts);
let E = lastStart;
for (let m = bodyStart; m < hi && list[m].offset < E; m++)
if (list[m].code === JUMP && list[m].target > E && list[m].target <= limit)
E = list[m].target;
if ([...starts, E].some((t) => !index.has(t)))
return null;
if (off(bodyStart) !== Math.min(...starts))
return null;
// If nothing breaks past the default label, there is no default: D is the end
const hasDefault = E > D;
const end = hasDefault ? E : D;
const inner = { brk: end, cont: loop ? loop.cont : undefined };
// Each body runs from its label to the next label; cases sharing a label share a body
const labels = [...new Set([...cases.map((c) => c.target), ...(hasDefault ? [D] : [])])].sort((a, b) => a - b);
const bodies = new Map();
labels.forEach((L, n) => {
const to = n + 1 < labels.length ? labels[n + 1] : end;
bodies.set(L, block(ctx, index.get(L), index.get(to), [], inner, false).stmts);
});
const groups = labels.map((L) => ({
values: cases.filter((c) => c.target === L).map((c) => c.value),
isDefault: hasDefault && L === D,
body: bodies.get(L),
}));
return { stmt: { k: 'switch', disc, groups }, next: index.get(end) };
}
// A for..in loop starts storeRegister 0, push null, equals2, if (exit), with its backward jump at j.
// If the body has a break, the break jumps to a little loop after the backward jump that pops the
// names left on the stack (push null, equals2, not, if back to itself), and the real exit is after it.
function forInShape(list, h, j, off) {
const isNull = (k) => list[k] && list[k].code === PUSH && list[k].values.length === 1 && list[k].values[0].type === 'null';
if (!(list[h] && list[h].code === STORE && list[h].register === 0 && isNull(h + 1) &&
list[h + 2] && list[h + 2].code === EQUALS2 && list[h + 3] && list[h + 3].code === IF))
return null;
const X = list[h + 3].target;
if (X === off(j + 1))
return { brk: X, next: j + 1 };
if (isNull(j + 1) && list[j + 2] && list[j + 2].code === EQUALS2 && list[j + 3] && list[j + 3].code === NOT &&
list[j + 4] && list[j + 4].code === IF && list[j + 4].target === off(j + 1) && X === off(j + 5))
return { brk: off(j + 1), next: j + 5 };
return null;
}
// A while loop that sets a variable just before it and changes it at the end of its body is a for loop
function forLoop(out, cond, body) {
const init = out[out.length - 1];
const update = body[body.length - 1];
const hasContinue = JSON.stringify(body).includes('"k":"continue"');
if (init && update && !hasContinue && init.k === 'assign' && update.k === 'assign' &&
same(init.target, update.target) && mentions(cond, init.target) && isStep(update)) {
out.pop();
return { k: 'for', init, cond, update, body: body.slice(0, -1) };
}
return { k: 'while', cond, body };
}
function isStep(s) {
return s.value.k === 'bin' && ['+', '-', '*', '/'].includes(s.value.op) && same(s.value.a, s.target);
}
function mentions(e, target) {
if (same(e, target))
return true;
for (const v of Object.values(e))
if (v && typeof v === 'object' && !Array.isArray(v) && mentions(v, target))
return true;
return false;
}
function same(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function hasEffect(e) {
return e.k === 'call' || e.k === 'new' || e.k === 'assignx' || e.k === 'postfix' || (e.k === 'un' && e.op === 'delete ');
}
// A register, named after the parameter or preloaded value it holds where there is one
function reg(ctx, n) {
return { k: 'reg', n, name: ctx.names.get(n) };
}
function callStatement(name, args = []) {
return { k: 'expr', e: call(variable(name), args) };
}
function getURLStatement(url, win) {
if (url.startsWith('FSCommand:'))
return callStatement('fscommand', [lit(url.slice(10)), lit(win)]);
const level = /^_level(\d+)$/.exec(win);
if (level && url === '')
return callStatement('unloadMovieNum', [lit(Number(level[1]))]);
if (level)
return callStatement('loadMovieNum', [lit(url), lit(Number(level[1]))]);
return callStatement('getURL', win ? [lit(url), lit(win)] : [lit(url)]);
}
// A function definition, with its body decompiled in its own register context
function functionNode(ins, ctx) {
const names = registerNames(ins);
const declared = new Set([...names.keys()]);
let list = ins.body;
let locals = ins.code === 0x8E;
if (ins.code === 0x9B) {
const unsaved = unsaveRegisters(list, ins.params.map((p) => p.name), names);
if (unsaved) {
list = unsaved;
locals = true;
for (const n of names.keys())
declared.add(n);
}
}
const body = decompile(list, { names, declared, fn2: locals });
return { k: 'fn', name: ins.fname, params: ins.params.map((p) => p.name), body };
}
// The Flash MX compiler keeps locals in registers even in a plain defineFunction. It saves the
// registers it uses on the stack (push r:1, r:2), copies this and the parameters into some of them,
// and puts the saved values back at the end. Every return jumps to that shared ending with its value
// on the stack:
//
// L: storeRegister r:0, pop, storeRegister r:2, pop, storeRegister r:1, pop, push r:0, return
//
// Taking the saving and restoring out, and turning each jump to L back into a return, leaves the
// function as it was written. Registers holding this or a parameter get its name. Returns null if
// the function isn't in this shape.
function unsaveRegisters(list, params, names) {
const first = list[0];
if (!first || first.code !== PUSH || !first.values.length ||
!first.values.every((v) => v.type === 'register' && v.value > 0))
return null;
const saved = first.values.map((v) => v.value);
const n = saved.length;
let end = list.length;
if (list[end - 1].code === 0x00)
end--;
// The restores come last saved first: storeRegister, pop for each
const restores = (k) => saved.every((r, m) => {
const s = list[k + 2 * (n - 1 - m)];
return s && s.code === STORE && s.register === r && list[k + 2 * (n - 1 - m) + 1].code === POP;
});
const isReg0 = (s) => s && s.code === PUSH && s.values.length === 1 && s.values[0].type === 'register' && s.values[0].value === 0;
const returns = end >= 2 && isReg0(list[end - 2]) && list[end - 1].code === 0x3E;
const E = returns ? end - 2 - 2 * n - 2 : end - 2 * n;
if (E < 1 || !restores(returns ? E + 2 : E))
return null;
if (returns && !(list[E].code === STORE && list[E].register === 0 && list[E + 1].code === POP))
return null;
if (list.some((s) => s.target === first.offset))
return null;
const exit = list[E].offset;
let body = list.slice(1, E);
// With return values, falling off the end pushes undefined for the shared ending to return
const last = body[body.length - 1];
if (returns && last && last.code === PUSH && last.values.length === 1 && last.values[0].type === 'undefined')
body = body.slice(0, -1);
if (returns)
body = body.map((s) => (s.code === JUMP && s.target === exit ? { ...s, code: 0x3E, name: 'return' } : s));
// push 'this' (or a parameter), getVariable, storeRegister r, pop: the register stands for that name
const assigned = (r, from) => body.some((s, k) => k >= from && s.code === STORE && s.register === r);
const aliases = new Map();
let k = 0;
while (body[k] && body[k].code === PUSH && body[k].values.length === 1 && body[k].values[0].type === 'string' &&
body[k + 1] && body[k + 1].code === 0x1C && body[k + 2] && body[k + 2].code === STORE &&
body[k + 3] && body[k + 3].code === POP) {
const name = body[k].values[0].value;
const r = body[k + 2].register;
if (!saved.includes(r) || names.has(r) || aliases.has(r) ||
!(params.includes(name) || (name === 'this' && !assigned(r, k + 3))))
break;
aliases.set(r, name);
k += 4;
}
if (body.slice(0, k).some((s) => body.some((t) => t.target === s.offset)))
return null;
for (const [r, name] of aliases)
names.set(r, name);
return body.slice(k);
}
function regName(ctx, n) {
return ctx.names.get(n) ?? `r${n}`;
}
// ---- Printing ----------------------------------------------------------------------------------------
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
// Prints statements as ActionScript, indented by depth
export function printStatements(stmts, names = new Map(), depth = 0) {
const lines = [];
for (const s of stmts)
printStatement(s, names, depth, lines);
return lines.join('\n');
}
function printStatement(s, names, depth, lines) {
const pad = ' '.repeat(depth);
const p = (e, prec = 0) => print(e, names, prec, depth);
const body = (stmts) => {
for (const t of stmts)
printStatement(t, names, depth + 1, lines);
};
switch (s.k) {
case 'expr':
lines.push(`${pad}${p(s.e)};`);
break;
case 'assign': {
// var a, b; compiles to registers set to undefined
if (s.declare && s.value.k === 'lit' && s.value.v === undefined)
lines.push(`${pad}var ${p(s.target)};`);
else
lines.push(`${pad}${s.declare ? 'var ' : ''}${printAssignment(s.target, s.value, names, depth)};`);
break;
}
case 'declare':
lines.push(`${pad}var ${p(s.target)};`);
break;
case 'return':
lines.push(s.e && s.e.k !== 'missing' && !(s.e.k === 'lit' && s.e.v === undefined) ? `${pad}return ${p(s.e)};` : `${pad}return;`);
break;
case 'throw':
lines.push(`${pad}throw ${p(s.e)};`);
break;
case 'if': {
lines.push(`${pad}if (${p(s.cond)}) {`);
body(s.then);
let els = s.else;
// else { if ... } is written as else if
while (els && els.length === 1 && els[0].k === 'if') {
lines.push(`${pad}} else if (${p(els[0].cond)}) {`);
body(els[0].then);
els = els[0].else;
}
if (els && els.length) {
lines.push(`${pad}} else {`);
body(els);
}
lines.push(`${pad}}`);
break;
}
case 'while':
lines.push(`${pad}while (${p(s.cond)}) {`);
body(s.body);
lines.push(`${pad}}`);
break;
case 'dowhile':
lines.push(`${pad}do {`);
body(s.body);
lines.push(`${pad}} while (${p(s.cond)});`);
break;
case 'for': {
const init = `${s.init.declare ? 'var ' : ''}${printAssignment(s.init.target, s.init.value, names, depth)}`;
const update = printAssignment(s.update.target, s.update.value, names, depth);
lines.push(`${pad}for (${init}; ${p(s.cond)}; ${update}) {`);
body(s.body);
lines.push(`${pad}}`);
break;
}
case 'forin':
lines.push(`${pad}for (${s.declare ? 'var ' : ''}${p(s.target)} in ${p(s.obj)}) {`);
body(s.body);
lines.push(`${pad}}`);
break;
case 'with':
lines.push(`${pad}with (${p(s.obj)}) {`);
body(s.body);
lines.push(`${pad}}`);
break;
case 'try':
lines.push(`${pad}try {`);
body(s.body);
if (s.catchBody) {
lines.push(`${pad}} catch (${s.catchName}) {`);
body(s.catchBody);
}
if (s.finallyBody) {
lines.push(`${pad}} finally {`);
body(s.finallyBody);
}
lines.push(`${pad}}`);
break;
case 'function': {
const fn = s.fn;
lines.push(`${pad}function ${fn.name}(${fn.params.join(', ')}) {`);
const inner = fnNames(fn);
for (const t of fn.body)
printStatement(t, inner, depth + 1, lines);
lines.push(`${pad}}`);
break;
}
case 'switch':
lines.push(`${pad}switch (${p(s.disc)}) {`);
for (const g of s.groups) {
for (const v of g.values)
lines.push(`${pad} case ${p(v)}:`);
if (g.isDefault)
lines.push(`${pad} default:`);
for (const t of g.body)
printStatement(t, names, depth + 2, lines);
}
lines.push(`${pad}}`);
break;
case 'extends':
lines.push(`${pad}${p(s.sub)} extends ${p(s.superclass)};`);
break;
case 'implements':
lines.push(`${pad}${p(s.ctor)} implements ${s.interfaces.map((e) => p(e)).join(', ')};`);
break;
case 'break':
lines.push(`${pad}break;`);
break;
case 'continue':
lines.push(`${pad}continue;`);
break;
case 'goto':
lines.push(`${pad}goto ${labelName(s.target)};`);
break;
case 'label':
lines.push(`${' '.repeat(Math.max(0, depth - 1))}${labelName(s.target)}:`);
break;
case 'raw':
lines.push(`${pad}${s.text}`);
break;
default:
lines.push(`${pad}// ${s.k}`);
}
}
// Assignments are written as x++, x += y and so on where they can be
function printAssignment(target, value, names, depth) {
const t = print(target, names, 0, depth);
if (value.k === 'bin' && same(value.a, target)) {
if (value.b.k === 'lit' && value.b.v === 1 && (value.op === '+' || value.op === '-'))
return `${t}${value.op}${value.op}`;
if (['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'].includes(value.op))
return `${t} ${value.op}= ${print(value.b, names, 1, depth)}`;
}
return `${t} = ${print(value, names, 1, depth)}`;
}
function fnNames(fn) {
return fn.names || new Map();
}
export function labelName(offset) {
return `label_${offset.toString(16).toUpperCase()}`;
}
// Prints an expression. prec is the precedence of the context, so brackets go in only where needed.
export function print(e, names = new Map(), prec = 0, depth = 0) {
const p = (x, q) => print(x, names, q, depth);
const wrap = (mine, text) => (mine < prec ? `(${text})` : text);
switch (e.k) {
case 'lit':
return literal(e.v);
case 'reg':
return e.name ?? `r${e.n}`;
case 'var':
return e.name;
case 'eval':
return `eval(${p(e.e, 0)})`;
case 'missing':
return '/*?*/';
case 'mem': {
const obj = p(e.obj, 14);
if (e.prop.k === 'lit' && typeof e.prop.v === 'string' && IDENTIFIER.test(e.prop.v))
return `${obj}.${e.prop.v}`;
return `${obj}[${p(e.prop, 0)}]`;
}
case 'prop':
// An empty target is the timeline the code is on
if (e.target.k === 'lit' && e.target.v === '')
return e.name;
return `${p(e.target, 14)}.${e.name}`;
case 'call': {
const fn = e.fn.k === 'fn' ? `(${p(e.fn, 0)})` : p(e.fn, 14);
return `${fn}(${e.args.map((a) => p(a, 1)).join(', ')})`;
}
case 'new':
return wrap(14, `new ${p(e.fn, 15)}(${e.args.map((a) => p(a, 1)).join(', ')})`);
case 'bin': {
// -x compiles as 0 - x
if (e.op === '-' && e.a.k === 'lit' && e.a.v === 0)
return wrap(13, `-${p(e.b, 13)}`);
const q = PRECEDENCE[e.op] ?? 9;
return wrap(q, `${p(e.a, q)} ${e.op} ${p(e.b, q + 1)}`);
}
case 'un':
return wrap(13, `${e.op}${p(e.a, 13)}`);
case 'logic': {
const q = PRECEDENCE[e.op];
return wrap(q, `${p(e.a, q)} ${e.op} ${p(e.b, q + 1)}`);
}
case 'cond':
return wrap(2, `${p(e.c, 3)} ? ${p(e.a, 2)} : ${p(e.b, 2)}`);
case 'assignx': {
// Used as a value, x = x + 1 is ++x
const v = e.value;
if (v.k === 'bin' && (v.op === '+' || v.op === '-') && v.b.k === 'lit' && v.b.v === 1 && same(v.a, e.target))
return wrap(13, `${v.op}${v.op}${p(e.target, 13)}`);
return wrap(1, printAssignment(e.target, e.value, names, depth));
}
case 'postfix':
return wrap(14, `${p(e.target, 14)}${e.op}`);
case 'array':
return `[${e.items.map((a) => p(a, 1)).join(', ')}]`;
case 'object':
if (!e.pairs.length)
return '{}';
return `{${e.pairs.map(([k, v]) => `${k.k === 'lit' && IDENTIFIER.test(String(k.v)) ? k.v : p(k, 0)}: ${p(v, 1)}`).join(', ')}}`;
case 'fn': {
const lines = [];
const inner = fnNames(e);
for (const t of e.body)
printStatement(t, inner, depth + 1, lines);
const head = `function${e.name ? ' ' + e.name : ''}(${e.params.join(', ')}) {`;
return lines.length ? `${head}\n${lines.join('\n')}\n${' '.repeat(depth)}}` : `${head}}`;
}
case 'enum':
return `/* enumerate ${p(e.obj, 0)} */`;
default:
return `/* ${e.k} */`;
}
}
function literal(v) {
if (v === undefined)
return 'undefined';
if (v === null)
return 'null';
if (typeof v === 'string')
return JSON.stringify(v);
if (typeof v === 'number') {
if (Number.isInteger(v))
return String(v);
// 32-bit floats print with a long tail: keep the digits a person would have typed
const short = Number(v.toPrecision(7));
return String(Math.fround(short) === Math.fround(v) && Math.abs(short - v) < 1e-6 * Math.abs(v) ? short : v);
}
return String(v);
}