decompile.js

JavaScript source for the SWF disassembler, 1346 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.

var registerNames = require('./actions').registerNames;

var NOT = 0x12, POP = 0x17, IF = 0x9D, JUMP = 0x99, PUSH = 0x96, STORE = 0x87, EQUALS2 = 0x49, PLAY = 0x06;

var 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]
var 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]
};

var 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
var 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
function NotAnExpression() {}

//  Decompiles a list of actions into statements. names maps registers to the names they hold.
function decompile(list, options) {
  options = options || {};
  var ctx = makeContext(list, options.names || {}, copy(options.declared || {}));
  ctx.fn2 = !!options.fn2;
  var result = block(ctx, 0, list.length, [], null, false);
  //  If gotos were needed, run again with labels at their targets
  if (Object.keys(ctx.gotos).length) {
    ctx.labels = copy(ctx.gotos);
    ctx.placed = {};
    ctx.declared = copy(options.declared || {});
    result = block(ctx, 0, list.length, [], null, false);
  }
  return result.stmts;
}

function makeContext(list, names, declared) {
  var index = {};
  list.forEach(function (ins, i) {
    index[ins.offset] = i;
  });
  var last = list[list.length - 1];
  var endOffset = last ? last.offset + last.size : 0;
  index[endOffset] = list.length;

  //  Every backward branch marks a loop: its target is the loop's first instruction
  var backEdges = {};
  list.forEach(function (ins, i) {
    if ((ins.code === JUMP || ins.code === IF) && ins.target <= ins.offset && index[ins.target] !== undefined) {
      var h = index[ins.target];
      if (backEdges[h] === undefined || backEdges[h] < i)
        backEdges[h] = i;
    }
  });

  return { list: list, index: index, endOffset: endOffset, backEdges: backEdges, names: names, declared: declared,
    labels: {}, placed: {}, gotos: {}, active: {} };
}

//  A shallow copy of an object, also used for the sets of offsets and registers kept as object keys
function copy(o) {
  var c = {};
  for (var key in o)
    if (o.hasOwnProperty(key))
      c[key] = o[key];
  return c;
}

function repeat(s, n) {
  return new Array(n + 1).join(s);
}

//  ---- Expression nodes --------------------------------------------------------------------------------

function lit(v) {
  return { k: 'lit', v: v };
}

function variable(name) {
  return { k: 'var', name: name };
}

function member(obj, prop) {
  return { k: 'mem', obj: obj, prop: prop };
}

function call(fn, args) {
  return { k: 'call', fn: fn, args: args };
}

function binary(op, a, b) {
  return { k: 'bin', op: op, a: a, b: b };
}

function unary(op, a) {
  return { k: 'un', op: op, a: 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') {
    var c = copy(e);
    c.a = simplifyCondition(e.a);
    c.b = simplifyCondition(e.b);
    return c;
  }
  if (e.k !== 'un' || e.op !== '!')
    return e;
  var 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') {
    var flip = { '==': '!=', '!=': '==', '===': '!==', '!==': '===', '<': '>=', '>': '<=', '<=': '>', '>=': '<', 'eq': 'ne', 'lt': 'ge', 'gt': 'le' };
    if (flip.hasOwnProperty(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: e };
}

function numberOf(e) {
  return e.k === 'lit' && typeof e.v === 'number' ? e.v : null;
}

//  A count pushed before a call or an array, which is 0 if it isn't a number
function countOf(e) {
  var n = numberOf(e);
  return n === null ? 0 : n;
}

function propertyName(n) {
  return PROPERTIES[n] !== undefined ? PROPERTIES[n] : 'property' + n;
}

//  ---- 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) {
  var list = ctx.list, index = ctx.index;
  var out = [];
  var off = function (i) {
    return i < list.length ? list[i].offset : ctx.endOffset;
  };
  var idx = function (offset) {
    return index[offset];
  };
  var emit = function (s) {
    if (exprMode)
      throw new NotAnExpression();
    out.push(s);
  };
  var pop = function () {
    return stack.length ? stack.pop() : { k: 'missing' };
  };
  var push = function (e) {
    stack.push(e);
  };
  var popArgs = function (n) {
    var args = [];
    for (var j = 0; j < n; j++)
      args.push(pop());
    return args;
  };
  var tryExpr = function (a, b) {
    try {
      var 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;
    }
  };
  var goTo = function (target) {
    ctx.gotos[target] = true;
    return { k: 'goto', target: target };
  };
  var jumpStatement = function (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.
  var chainPending = false;
  var chainValue = null;
  var chained = function (target, value) {
    if (!chainPending)
      return false;
    chainPending = false;
    chainValue = { k: 'assignx', target: target, value: 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++.
  var postfix = function (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 (var n = stack.length - 1; n >= 0; n--)
      if (same(stack[n], target)) {
        stack[n] = { k: 'postfix', op: value.op + value.op, target: target };
        return true;
      }
    return false;
  };

  var i = lo;
  var ins, j, r, k, m, n, e, a, b, value, target, prop, obj, fn, args, sw, T, t, E;
  while (i < hi) {
    ins = list[i];
    if (ctx.labels[ins.offset] && !ctx.placed[ins.offset] && !exprMode) {
      ctx.placed[ins.offset] = true;
      out.push({ k: 'label', target: ins.offset });
    }

    //  ---- Loops: this instruction is the target of a later backward branch inside this range
    j = ctx.backEdges[i];
    if (j !== undefined && j < hi && !ctx.active[i] && !exprMode) {
      ctx.active[i] = true;
      var exit = off(j + 1);
      var inner = { brk: exit, cont: off(i) };
      var back = list[j];
      var next = j + 1;
      var 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
        r = block(ctx, i, j, [], inner, false);
        emit({ k: 'dowhile', body: r.stmts, cond: r.stack.length ? r.stack.pop() : lit(true) });
      } else if (forIn) {
        //  for (x in obj): enumerate pushed a null and then each name, and the loop pops them in turn
        obj = pop().obj;
        r = block(ctx, i + 4, j, [], { brk: forIn.brk, cont: off(i) }, false);
        next = forIn.next;
        var body = r.stmts;
        target = { k: 'var', name: '?' };
        var 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: target, declare: declare, obj: obj, body: body });
      } else {
        //  while (cond) { body }: the condition is tested at the top, and exits past the backward jump
        k = -1;
        for (m = i; m < j; m++)
          if (list[m].code === IF && list[m].target === exit) {
            k = m;
            break;
          }
        var cond = k >= 0 ? tryExpr(i, k) : null;
        if (cond) {
          r = block(ctx, k + 1, j, [], inner, false);
          emit(forLoop(out, negate(cond), r.stmts));
        } else {
          r = block(ctx, i, j, [], inner, false);
          emit({ k: 'while', cond: lit(true), body: r.stmts });
        }
      }
      delete ctx.active[i];
      i = next;
      continue;
    }

    switch (ins.code) {
      case 0x00:  //  end
      case 0x88:  //  constantPool
        break;

      case PUSH:
        ins.values.forEach(function (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:
        e = pop();
        if (e.k !== 'missing' && e.k !== 'enum' && hasEffect(e))
          emit({ k: 'expr', e: e });
        break;

      case 0x4C:  //  pushDuplicate: the start of a && or || if a branch and a pop follow
        k = i + 1;
        var 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) {
          var end = idx(list[k].target);
          if (end !== undefined && end <= hi) {
            var right = tryExpr(k + 2, end);
            if (right) {
              push({ k: 'logic', op: and ? '&&' : '||', a: pop(), b: right });
              i = end;
              continue;
            }
          }
        }
        var top = pop();
        push(top);
        push(top);
        break;

      case 0x4D:  //  stackSwap
        a = pop();
        b = pop();
        push(a);
        push(b);
        break;

      case 0x1C:  //  getVariable
        push(nameOf(pop()));
        break;

      case 0x1D:  //  setVariable
        value = pop();
        target = nameOf(pop());
        if (!chained(target, value) && !postfix(target, value))
          emit({ k: 'assign', target: target, value: value });
        break;

      case 0x3C:  //  defineLocal
        value = pop();
        emit({ k: 'assign', target: nameOf(pop()), value: value, declare: true });
        break;

      case 0x41:  //  defineLocal2
        emit({ k: 'declare', target: nameOf(pop()) });
        break;

      case 0x4E:  //  getMember
        prop = pop();
        push(member(pop(), prop));
        break;

      case 0x4F:  //  setMember
        value = pop();
        prop = pop();
        target = member(pop(), prop);
        if (!chained(target, value) && !postfix(target, value))
          emit({ k: 'assign', target: target, value: value });
        break;

      case STORE:  //  storeRegister: copies the top of the stack into a register, leaving it there
        if (!exprMode) {
          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
        var nextSet = list[i + 1] && (list[i + 1].code === 0x4F || list[i + 1].code === 0x1D);
        var 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;
        }
        value = pop();
        r = reg(ctx, ins.register);
        if (exprMode) {
          push({ k: 'assignx', target: r, value: value });
          break;
        }
        var popped = list[i + 1] && list[i + 1].code === POP && i + 1 < hi;
        if (popped && postfix(r, value)) {
          i += 2;
          continue;
        }
        var declared = ctx.fn2 && !ctx.declared[ins.register];
        if (declared)
          ctx.declared[ins.register] = true;
        emit({ k: 'assign', target: r, value: value, declare: declared });
        if (popped) {
          i += 2;
          continue;
        }
        push(r);
        break;

      case 0x3D:  //  callFunction
        fn = nameOf(pop());
        n = countOf(pop());
        push(call(fn, popArgs(n)));
        break;

      case 0x52:  //  callMethod
      case 0x53:  //  newMethod
        var name = pop();
        obj = pop();
        n = countOf(pop());
        args = popArgs(n);
        fn = name.k === 'lit' && (name.v === undefined || name.v === '') ? obj : member(obj, name);
        push(ins.code === 0x52 ? call(fn, args) : { k: 'new', fn: fn, args: args });
        break;

      case 0x40:  //  newObject
        fn = nameOf(pop());
        n = countOf(pop());
        push({ k: 'new', fn: fn, args: popArgs(n) });
        break;

      case 0x42:  //  initArray
        n = countOf(pop());
        push({ k: 'array', items: popArgs(n) });
        break;

      case 0x43:  //  initObject
        n = countOf(pop());
        var pairs = [];
        for (m = 0; m < n; m++) {
          value = pop();
          pairs.unshift([pop(), value]);
        }
        push({ k: 'object', pairs: 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
        prop = pop();
        push(unary('delete ', member(pop(), prop)));
        break;

      case 0x3B:  //  delete2
        push(unary('delete ', nameOf(pop())));
        break;

      case 0x22:  //  getProperty
        prop = numberOf(pop());
        target = pop();
        push({ k: 'prop', target: target, name: propertyName(prop) });
        break;

      case 0x23:  //  setProperty
        value = pop();
        prop = numberOf(pop());
        target = pop();
        emit({ k: 'assign', target: { k: 'prop', target: target, name: propertyName(prop) }, value: value });
        break;

      case 0x15:  //  stringExtract
      case 0x35:  //  mbStringExtract
        var count = pop(), at = pop(), s = pop();
        push(call(variable(ins.code === 0x15 ? 'substring' : 'mbsubstring'), [s, at, count]));
        break;

      case 0x34:  //  getTime
        push(call(variable('getTimer'), []));
        break;

      case 0x2B:  //  castOp
        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
        var superclass = pop();
        emit({ k: 'extends', sub: pop(), superclass: superclass });
        break;

      case 0x2C:  //  implementsOp
        var ctor = pop();
        n = countOf(pop());
        emit({ k: 'implements', ctor: ctor, interfaces: popArgs(n) });
        break;

      case 0x9B:  //  defineFunction
      case 0x8E:  //  defineFunction2
        fn = functionNode(ins, ctx);
        if (ins.fname)
          emit({ k: 'function', fn: fn });
        else
          push(fn);
        break;

      case 0x94:  //  with
        obj = pop();
        emit({ k: 'with', obj: obj, body: decompile(ins.body, { names: ctx.names, declared: ctx.declared }) });
        break;

      case 0x8F:  //  try
        var 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
        target = pop();
        var url = pop();
        args = [url, target];
        if (ins.method !== 'none')
          args.push(lit(ins.method));
        if (ins.loadVariables)
          emit(callStatement(ins.loadTarget ? 'loadVariables' : 'loadVariablesNum', args));
        else if (ins.loadTarget)
          emit(callStatement('loadMovie', args));
        else
          emit(callStatement('getURL', args));
        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'
        target = pop();
        var lock = pop(), constrain = pop();
        args = [target, lock];
        if (constrain.k !== 'lit' || Number(constrain.v)) {
          var y2 = pop(), x2 = pop(), y1 = pop(), x1 = pop();
          args.push(x1, y1, x2, y2);
        }
        emit(callStatement('startDrag', args));
        break;

      case 0x24:  //  cloneSprite
        var depth = pop();
        target = pop();
        var source = pop();
        emit(callStatement('duplicateMovieClip', [source, target, depth]));
        break;

      case 0x2D:  //  fsCommand2
        emit(callStatement('fscommand2', popArgs(countOf(pop()))));
        break;

      //  ---- Branches

      case IF:
        value = pop();
        T = ins.target;
        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 === '===') {
          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) {
          e = idx(list[t - 1].target);
          if (e !== undefined && e <= hi) {
            var ifFalse = tryExpr(i + 1, t - 1);
            var 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) {
          E = list[t - 1].target;
          e = idx(E);
          if (e !== undefined && e <= hi) {
            a = block(ctx, i + 1, t - 1, [], loop, false);
            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:
        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]) {
          b = pop();
          push(binary(BINARY[ins.code][0], pop(), b));
        } else if (BUILTIN_1[ins.code]) {
          e = call(variable(BUILTIN_1[ins.code]), [pop()]);
          if (ins.code === 0x25 || ins.code === 0x26)
            emit({ k: 'expr', e: e });
          else
            push(e);
        } else {
          emit({ k: 'raw', text: '//  ' + ins.name });
        }
    }
    i++;
  }

  return { stmts: out, stack: 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) {
  var list = ctx.list;
  var STRICT = 0x66;
  var r = list[i].register;
  var isCase = function (k, first) {
    return 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;

  var cases = [];
  var k = i + 1;
  var first = true;
  while (isCase(k, first)) {
    var 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) {
  var list = ctx.list;
  var disc = first.a;
  var cases = [{ value: first.b, target: list[i].target }];
  var k = i + 1;
  while (k < hi && list[k].code !== JUMP) {
    var 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;
    var e = null;
    try {
      var 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) {
  var list = ctx.list, index = ctx.index;
  if (!list[k] || list[k].code !== JUMP || list[k].target <= list[k].offset)
    return null;
  var D = list[k].target;
  var bodyStart = k + 1;
  var off = function (n) {
    return n < list.length ? list[n].offset : ctx.endOffset;
  };
  var limit = off(hi);

  //  The end is where the breaks go: the furthest forward jump that stays inside this range
  var starts = cases.map(function (c) { return c.target; }).concat([D]);
  var lastStart = Math.max.apply(Math, starts);
  var E = lastStart;
  for (var 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.concat([E]).some(function (t) { return index[t] === undefined; }))
    return null;
  if (off(bodyStart) !== Math.min.apply(Math, starts))
    return null;

  //  If nothing breaks past the default label, there is no default: D is the end
  var hasDefault = E > D;
  var end = hasDefault ? E : D;
  var 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
  var labels = [];
  cases.forEach(function (c) {
    if (labels.indexOf(c.target) < 0)
      labels.push(c.target);
  });
  if (hasDefault && labels.indexOf(D) < 0)
    labels.push(D);
  labels.sort(function (a, b) { return a - b; });
  var bodies = {};
  labels.forEach(function (L, n) {
    var to = n + 1 < labels.length ? labels[n + 1] : end;
    bodies[L] = block(ctx, index[L], index[to], [], inner, false).stmts;
  });

  var groups = labels.map(function (L) {
    return {
      values: cases.filter(function (c) { return c.target === L; }).map(function (c) { return c.value; }),
      isDefault: hasDefault && L === D,
      body: bodies[L]
    };
  });
  return { stmt: { k: 'switch', disc: disc, groups: groups }, next: index[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) {
  var isNull = function (k) {
    return 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;
  var 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) {
  var init = out[out.length - 1];
  var update = body[body.length - 1];
  var hasContinue = JSON.stringify(body).indexOf('"k":"continue"') >= 0;
  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: init, cond: cond, update: update, body: body.slice(0, -1) };
  }
  return { k: 'while', cond: cond, body: body };
}

function isStep(s) {
  return s.value.k === 'bin' && ['+', '-', '*', '/'].indexOf(s.value.op) >= 0 && same(s.value.a, s.target);
}

function mentions(e, target) {
  if (same(e, target))
    return true;
  return Object.keys(e).some(function (key) {
    var v = e[key];
    return v && typeof v === 'object' && !Array.isArray(v) && mentions(v, target);
  });
}

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: n, name: ctx.names[n] };
}

function callStatement(name, args) {
  return { k: 'expr', e: call(variable(name), args || []) };
}

function getURLStatement(url, win) {
  if (url.indexOf('FSCommand:') === 0)
    return callStatement('fscommand', [lit(url.slice(10)), lit(win)]);
  var 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) {
  var names = registerNames(ins);
  var declared = {};
  var n;
  for (n in names)
    declared[n] = true;
  var list = ins.body;
  var locals = ins.code === 0x8E;
  if (ins.code === 0x9B) {
    var unsaved = unsaveRegisters(list, ins.params.map(function (p) { return p.name; }), names);
    if (unsaved) {
      list = unsaved;
      locals = true;
      for (n in names)
        declared[n] = true;
    }
  }
  var body = decompile(list, { names: names, declared: declared, fn2: locals });
  return { k: 'fn', name: ins.fname, params: ins.params.map(function (p) { return p.name; }), body: 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) {
  var first = list[0];
  if (!first || first.code !== PUSH || !first.values.length ||
      !first.values.every(function (v) { return v.type === 'register' && v.value > 0; }))
    return null;
  var saved = first.values.map(function (v) { return v.value; });
  var n = saved.length;
  var end = list.length;
  if (list[end - 1].code === 0x00)
    end--;

  //  The restores come last saved first: storeRegister, pop for each
  var restores = function (k) {
    return saved.every(function (r, m) {
      var s = list[k + 2 * (n - 1 - m)];
      return s && s.code === STORE && s.register === r && list[k + 2 * (n - 1 - m) + 1].code === POP;
    });
  };
  var isReg0 = function (s) {
    return s && s.code === PUSH && s.values.length === 1 && s.values[0].type === 'register' && s.values[0].value === 0;
  };
  var returns = end >= 2 && isReg0(list[end - 2]) && list[end - 1].code === 0x3E;
  var 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(function (s) { return s.target === first.offset; }))
    return null;

  var exit = list[E].offset;
  var body = list.slice(1, E);
  //  With return values, falling off the end pushes undefined for the shared ending to return
  var 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(function (s) {
      if (s.code !== JUMP || s.target !== exit)
        return s;
      var ret = copy(s);
      ret.code = 0x3E;
      ret.name = 'return';
      return ret;
    });

  //  push 'this' (or a parameter), getVariable, storeRegister r, pop: the register stands for that name
  var assigned = function (r, from) {
    return body.some(function (s, k) { return k >= from && s.code === STORE && s.register === r; });
  };
  var aliases = {};
  var 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) {
    var name = body[k].values[0].value;
    var r = body[k + 2].register;
    if (saved.indexOf(r) < 0 || names.hasOwnProperty(r) || aliases.hasOwnProperty(r) ||
        !(params.indexOf(name) >= 0 || (name === 'this' && !assigned(r, k + 3))))
      break;
    aliases[r] = name;
    k += 4;
  }
  if (body.slice(0, k).some(function (s) { return body.some(function (t) { return t.target === s.offset; }); }))
    return null;
  for (r in aliases)
    names[r] = aliases[r];
  return body.slice(k);
}

function regName(ctx, n) {
  return ctx.names[n] !== undefined ? ctx.names[n] : 'r' + n;
}

//  ---- Printing ----------------------------------------------------------------------------------------

var IDENTIFIER = /^[A-Za-z_$][\w$]*$/;

//  Prints statements as ActionScript, indented by depth
function printStatements(stmts, names, depth) {
  var lines = [];
  stmts.forEach(function (s) {
    printStatement(s, names || {}, depth || 0, lines);
  });
  return lines.join('\n');
}

function printStatement(s, names, depth, lines) {
  var pad = repeat('  ', depth);
  var p = function (e, prec) {
    return print(e, names, prec || 0, depth);
  };
  var body = function (stmts) {
    stmts.forEach(function (t) {
      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);
      var 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':
      var init = (s.init.declare ? 'var ' : '') + printAssignment(s.init.target, s.init.value, names, depth);
      var 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':
      var fn = s.fn;
      lines.push(pad + 'function ' + fn.name + '(' + fn.params.join(', ') + ') {');
      var inner = fnNames(fn);
      fn.body.forEach(function (t) {
        printStatement(t, inner, depth + 1, lines);
      });
      lines.push(pad + '}');
      break;

    case 'switch':
      lines.push(pad + 'switch (' + p(s.disc) + ') {');
      s.groups.forEach(function (g) {
        g.values.forEach(function (v) {
          lines.push(pad + '  case ' + p(v) + ':');
        });
        if (g.isDefault)
          lines.push(pad + '  default:');
        g.body.forEach(function (t) {
          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(function (e) { return 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) {
  var 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 (['+', '-', '*', '/', '%', '&', '|', '^', '<<', '>>', '>>>'].indexOf(value.op) >= 0)
      return t + ' ' + value.op + '= ' + print(value.b, names, 1, depth);
  }
  return t + ' = ' + print(value, names, 1, depth);
}

function fnNames(fn) {
  return fn.names || {};
}

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.
function print(e, names, prec, depth) {
  names = names || {};
  prec = prec || 0;
  depth = depth || 0;
  var p = function (x, q) {
    return print(x, names, q, depth);
  };
  var wrap = function (mine, text) {
    return mine < prec ? '(' + text + ')' : text;
  };
  var q;

  switch (e.k) {
    case 'lit':
      return literal(e.v);
    case 'reg':
      return e.name !== undefined ? e.name : 'r' + e.n;
    case 'var':
      return e.name;
    case 'eval':
      return 'eval(' + p(e.e, 0) + ')';
    case 'missing':
      return '/*?*/';
    case 'mem':
      var 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':
      var fn = e.fn.k === 'fn' ? '(' + p(e.fn, 0) + ')' : p(e.fn, 14);
      return fn + '(' + e.args.map(function (a) { return p(a, 1); }).join(', ') + ')';
    case 'new':
      return wrap(14, 'new ' + p(e.fn, 15) + '(' + e.args.map(function (a) { return 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));
      q = PRECEDENCE.hasOwnProperty(e.op) ? 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':
      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
      var 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(function (a) { return p(a, 1); }).join(', ') + ']';
    case 'object':
      if (!e.pairs.length)
        return '{}';
      return '{' + e.pairs.map(function (pair) {
        var k = pair[0];
        return (k.k === 'lit' && IDENTIFIER.test(String(k.v)) ? k.v : p(k, 0)) + ': ' + p(pair[1], 1);
      }).join(', ') + '}';
    case 'fn':
      var lines = [];
      var inner = fnNames(e);
      e.body.forEach(function (t) {
        printStatement(t, inner, depth + 1, lines);
      });
      var 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 + ' */';
  }
}

//  Rounds to the nearest 32-bit float, the precision a push stores most numbers in
var float32 = new Float32Array(1);

function toFloat32(v) {
  float32[0] = v;
  return float32[0];
}

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 (v % 1 === 0)
      return String(v);
    //  32-bit floats print with a long tail: keep the digits a person would have typed
    var short = Number(v.toPrecision(7));
    return String(toFloat32(short) === toFloat32(v) && Math.abs(short - v) < 1e-6 * Math.abs(v) ? short : v);
  }
  return String(v);
}

exports.decompile = decompile;
exports.printStatements = printStatements;
exports.labelName = labelName;
exports.print = print;