/*
 * Copyright 2016 Miroslav Janíček
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.classdump.luna.compiler;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.classdump.luna.compiler.ir.AbstractVal;
import org.classdump.luna.compiler.ir.AbstractVar;
import org.classdump.luna.compiler.ir.BinOp;
import org.classdump.luna.compiler.ir.Branch;
import org.classdump.luna.compiler.ir.Call;
import org.classdump.luna.compiler.ir.Closure;
import org.classdump.luna.compiler.ir.CodeBuilder;
import org.classdump.luna.compiler.ir.Jmp;
import org.classdump.luna.compiler.ir.Label;
import org.classdump.luna.compiler.ir.LoadConst;
import org.classdump.luna.compiler.ir.MultiGet;
import org.classdump.luna.compiler.ir.MultiVal;
import org.classdump.luna.compiler.ir.PhiLoad;
import org.classdump.luna.compiler.ir.PhiStore;
import org.classdump.luna.compiler.ir.PhiVal;
import org.classdump.luna.compiler.ir.RegProvider;
import org.classdump.luna.compiler.ir.Ret;
import org.classdump.luna.compiler.ir.TCall;
import org.classdump.luna.compiler.ir.TabGet;
import org.classdump.luna.compiler.ir.TabNew;
import org.classdump.luna.compiler.ir.TabRawAppendMulti;
import org.classdump.luna.compiler.ir.TabRawSet;
import org.classdump.luna.compiler.ir.TabRawSetInt;
import org.classdump.luna.compiler.ir.TabSet;
import org.classdump.luna.compiler.ir.ToNumber;
import org.classdump.luna.compiler.ir.UnOp;
import org.classdump.luna.compiler.ir.UpLoad;
import org.classdump.luna.compiler.ir.UpStore;
import org.classdump.luna.compiler.ir.UpVar;
import org.classdump.luna.compiler.ir.VList;
import org.classdump.luna.compiler.ir.Val;
import org.classdump.luna.compiler.ir.Var;
import org.classdump.luna.compiler.ir.VarInit;
import org.classdump.luna.compiler.ir.VarLoad;
import org.classdump.luna.compiler.ir.VarStore;
import org.classdump.luna.compiler.ir.Vararg;
import org.classdump.luna.parser.analysis.FunctionVarInfo;
import org.classdump.luna.parser.analysis.ResolvedLabel;
import org.classdump.luna.parser.analysis.ResolvedVariable;
import org.classdump.luna.parser.analysis.VarMapping;
import org.classdump.luna.parser.analysis.Variable;
import org.classdump.luna.parser.ast.AssignStatement;
import org.classdump.luna.parser.ast.BinaryOperationExpr;
import org.classdump.luna.parser.ast.Block;
import org.classdump.luna.parser.ast.BodyStatement;
import org.classdump.luna.parser.ast.BooleanLiteral;
import org.classdump.luna.parser.ast.BreakStatement;
import org.classdump.luna.parser.ast.CallExpr;
import org.classdump.luna.parser.ast.CallStatement;
import org.classdump.luna.parser.ast.Chunk;
import org.classdump.luna.parser.ast.ConditionalBlock;
import org.classdump.luna.parser.ast.Expr;
import org.classdump.luna.parser.ast.FunctionDefExpr;
import org.classdump.luna.parser.ast.GenericForStatement;
import org.classdump.luna.parser.ast.GotoStatement;
import org.classdump.luna.parser.ast.IfStatement;
import org.classdump.luna.parser.ast.IndexExpr;
import org.classdump.luna.parser.ast.LValueExpr;
import org.classdump.luna.parser.ast.LabelStatement;
import org.classdump.luna.parser.ast.Literal;
import org.classdump.luna.parser.ast.LiteralExpr;
import org.classdump.luna.parser.ast.LocalDeclStatement;
import org.classdump.luna.parser.ast.MultiExpr;
import org.classdump.luna.parser.ast.NilLiteral;
import org.classdump.luna.parser.ast.Numeral;
import org.classdump.luna.parser.ast.NumericForStatement;
import org.classdump.luna.parser.ast.Operator;
import org.classdump.luna.parser.ast.ParenExpr;
import org.classdump.luna.parser.ast.RepeatUntilStatement;
import org.classdump.luna.parser.ast.ReturnStatement;
import org.classdump.luna.parser.ast.StringLiteral;
import org.classdump.luna.parser.ast.TableConstructorExpr;
import org.classdump.luna.parser.ast.Transformer;
import org.classdump.luna.parser.ast.UnaryOperationExpr;
import org.classdump.luna.parser.ast.VarExpr;
import org.classdump.luna.parser.ast.VarargsExpr;
import org.classdump.luna.parser.ast.WhileStatement;
import org.classdump.luna.parser.ast.util.AttributeUtils;

class IRTranslatorTransformer extends Transformer {

  private final ModuleBuilder moduleBuilder;

  private final FunctionId id;

  private final CodeBuilder insns;

  private final RegProvider provider;
  private final Deque<AbstractVal> vals;
  private final Deque<MultiVal> multis;

  private final Deque<Label> breakLabels;
  private final Map<ResolvedLabel, Label> userLabels;
  private final Map<Variable, Var> vars;
//	private boolean onStack;
  private final Map<Variable.Ref, UpVar> uvs;
  private final List<Var> params;
  private final List<UpVar> upvals;
  private boolean assigning;
  private boolean vararg;
  private int nextNestedFnIdx;

  private IRTranslatorTransformer(ModuleBuilder moduleBuilder, FunctionId id) {
    this.moduleBuilder = Objects.requireNonNull(moduleBuilder);
    this.id = Objects.requireNonNull(id);

    this.provider = new RegProvider();

    this.insns = new CodeBuilder();

    this.vals = new ArrayDeque<>();
    this.multis = new ArrayDeque<>();

    this.assigning = false;
//		this.onStack = false;
    this.breakLabels = new ArrayDeque<>();
    this.userLabels = new HashMap<>();

    this.vars = new HashMap<>();
    this.uvs = new HashMap<>();

    this.params = new ArrayList<>();
    this.vararg = false;
    this.upvals = new ArrayList<>();

    this.nextNestedFnIdx = 0;
  }

  public IRTranslatorTransformer(ModuleBuilder moduleBuilder) {
    this(moduleBuilder, FunctionId.root());
  }

  private IRFunc result() {
    return new IRFunc(id,
        Collections.unmodifiableList(params),
        vararg,
        Collections.unmodifiableList(upvals),
        insns.build());
  }

  private boolean onStack() {
    return !multis.isEmpty();
  }

  private Val popVal() {
    if (!vals.isEmpty()) {
      AbstractVal aval = vals.pop();
      if (aval instanceof PhiVal) {
        Val v = provider.newVal();
        insns.add(new PhiLoad(v, (PhiVal) aval));
        return v;
      } else if (aval instanceof Val) {
        return (Val) aval;
      } else {
        throw new UnsupportedOperationException("Unknown abstract value: " + aval);
      }
    } else if (!multis.isEmpty()) {
      Val v = provider.newVal();
      MultiVal mv = multis.pop();
      insns.add(new MultiGet(v, mv, 0));
      return v;
    } else {
      throw new IllegalStateException("no value on stack");
    }
  }

  private Var var(Variable v) {
    Var w = vars.get(v);
    if (w != null) {
      return w;
    } else {
      w = provider.newVar();
      vars.put(v, w);
      return w;
    }
  }

  private UpVar upVar(Variable.Ref v) {
    UpVar w = uvs.get(v);
    if (w != null) {
      return w;
    } else {
      w = provider.newUpVar(v.var().name());
      uvs.put(v, w);
      return w;
    }
  }

  private Label userLabel(ResolvedLabel rl) {
    Label l = userLabels.get(rl);
    if (l != null) {
      return l;
    } else {
      l = insns.newLabel();
      userLabels.put(rl, l);
      return l;
    }
  }

  private void endStatement() {
    multis.clear();
    vals.clear();
  }

  @Override
  public LValueExpr transform(VarExpr e) {
    ResolvedVariable rv = TranslationUtils.resolved(e);

    insns.atLine(e.line());

    if (rv.isUpvalue()) {
      Variable.Ref ref = rv.variable().ref();
      // upvalue
      if (assigning) {
        Val src = popVal();
        insns.add(new UpStore(upVar(ref), src));
      } else {
        Val dest = provider.newVal();
        vals.push(dest);
        insns.add(new UpLoad(dest, upVar(ref)));
      }
    } else {
      Variable var = rv.variable();
      // local variable
      if (assigning) {
        Val src = popVal();
        insns.add(new VarStore(var(var), src));
      } else {
        Val dest = provider.newVal();
        vals.push(dest);
        insns.add(new VarLoad(dest, var(var)));
      }
    }

    return e;
  }

  @Override
  public LValueExpr transform(IndexExpr e) {
    // pop the value first, if we are assigning, this avoids confusing the val
    // on the stack with potential multival returns from object or key
    Val value = null;
    if (assigning) {
      value = popVal();
    }
    boolean as = assigning;
    assigning = false;

    e.object().accept(this);
    Val obj = popVal();
    e.key().accept(this);
    Val key = popVal();

    assigning = as;

    insns.atLine(e.line());

    if (assigning) {
      insns.add(new TabSet(obj, key, value));
    } else {
      Val dest = provider.newVal();
      vals.push(dest);
      insns.add(new TabGet(dest, obj, key));
    }

    return e;
  }

  @Override
  public Expr transform(LiteralExpr e) {
    insns.atLine(e.line());
    return super.transform(e);
  }

  @Override
  public Literal transform(NilLiteral l) {
    Val dest = provider.newVal();
    vals.push(dest);
    insns.add(new LoadConst.Nil(dest));
    return l;
  }

  @Override
  public Literal transform(BooleanLiteral l) {
    Val dest = provider.newVal();
    vals.push(dest);
    insns.add(new LoadConst.Bool(dest, l.value()));
    return l;
  }

  @Override
  public Literal transform(Numeral.IntegerNumeral l) {
    Val dest = provider.newVal();
    vals.push(dest);
    insns.add(new LoadConst.Int(dest, l.value()));
    return l;
  }

  @Override
  public Literal transform(Numeral.FloatNumeral l) {
    Val dest = provider.newVal();
    vals.push(dest);
    insns.add(new LoadConst.Flt(dest, l.value()));
    return l;
  }

  @Override
  public Literal transform(StringLiteral l) {
    Val dest = provider.newVal();
    vals.push(dest);
    insns.add(new LoadConst.Str(dest, l.value()));
    return l;
  }

  private void and(Expr left, Expr right) {
    PhiVal dest = provider.newPhiVal();
    Label l_false = insns.newLabel();
    Label l_done = insns.newLabel();

    left.accept(this);
    Val l = popVal();

    // TODO: line

    insns.addBranch(new Branch.Condition.Bool(l, false), l_false);

    right.accept(this);
    Val r = popVal();
    insns.add(new PhiStore(dest, r));
    insns.add(new Jmp(l_done));

    insns.add(l_false);
    insns.add(new PhiStore(dest, l));

    insns.add(l_done);
    vals.push(dest);
  }

  private void or(Expr left, Expr right) {
    PhiVal dest = provider.newPhiVal();
    Label l_true = insns.newLabel();
    Label l_done = insns.newLabel();

    left.accept(this);
    Val l = popVal();

    // TODO: line

    insns.addBranch(new Branch.Condition.Bool(l, true), l_true);

    right.accept(this);
    Val r = popVal();
    insns.add(new PhiStore(dest, r));
    insns.add(new Jmp(l_done));

    insns.add(l_true);
    insns.add(new PhiStore(dest, l));

    insns.add(l_done);
    vals.push(dest);
  }

  private void eagerBinOp(Operator.Binary op, Expr left, Expr right) {
    BinOp.Op bop = TranslationUtils.bop(op);
    boolean swap = false;

    if (bop == null) {
      bop = TranslationUtils.bop(op.swap());
      swap = true;
    }

    if (bop == null) {
      throw new UnsupportedOperationException("Binary operator not supported: " + op);
    }

    left.accept(this);
    Val l = popVal();
    right.accept(this);
    Val r = popVal();

    Val dest = provider.newVal();
    vals.push(dest);

    // TODO: line

    insns.add(new BinOp(bop, dest, swap ? r : l, swap ? l : r));
  }

  @Override
  public Expr transform(BinaryOperationExpr e) {
    switch (e.op()) {
      case AND:
        and(e.left(), e.right());
        break;
      case OR:
        or(e.left(), e.right());
        break;
      default:
        eagerBinOp(e.op(), e.left(), e.right());
        break;
    }
    return e;
  }

  @Override
  public Expr transform(UnaryOperationExpr e) {
    e.arg().accept(this);

    Val arg = popVal();
    Val dest = provider.newVal();
    vals.push(dest);

    insns.atLine(e.line());

    insns.add(new UnOp(TranslationUtils.uop(e.op()), dest, arg));
    return e;
  }

  @Override
  public Expr transform(VarargsExpr e) {
    insns.atLine(e.line());
    MultiVal mv = provider.newMultiVal();
    insns.add(new Vararg(mv));
    multis.push(mv);
    return e;
  }

  @Override
  public Expr transform(ParenExpr e) {
    e.multiExpr().accept(this);

    if (multis.isEmpty()) {
      throw new IllegalStateException("empty multival stack");
    }

    Val dest = provider.newVal();
    MultiVal mv = multis.pop();
    insns.add(new MultiGet(dest, mv, 0));

    vals.push(dest);

//		Val dest = popVal();
//		vals.push(dest);

    return e;
  }

  private VList vlist(Val prefix, List<Expr> exprs) {
    List<Val> as = new ArrayList<>();
    if (prefix != null) {
      as.add(prefix);
    }

    MultiVal multi = null;
    Iterator<Expr> it = exprs.iterator();
    while (it.hasNext()) {
      Expr e = it.next();
      e.accept(this);

      if (e instanceof MultiExpr && !it.hasNext() && onStack()) {
        // multi-value expression in tail position
        MultiVal mv = multis.pop();
        multis.clear();  // FIXME
        multi = mv;
      } else {
        // single value
        as.add(popVal());
      }
    }

    return new VList(Collections.unmodifiableList(as), multi);
  }

  private VList vlist(List<Expr> exprs) {
    return vlist(null, exprs);
  }

  private Call call(CallExpr.FunctionCallExpr e) {
    e.fn().accept(this);
    Val fn = popVal();
    VList vl = vlist(e.args());

    MultiVal result = provider.newMultiVal();
    return new Call(result, fn, vl);
  }

  private Call call(CallExpr.MethodCallExpr e) {
    e.target().accept(this);
    Val obj = popVal();

    transform(StringLiteral.fromName(e.methodName()));
    Val method = popVal();

    Val fn = provider.newVal();
    insns.add(new TabGet(fn, obj, method));

    VList vl = vlist(obj, e.args());

    MultiVal result = provider.newMultiVal();
    return new Call(result, fn, vl);
  }

  @Override
  public Expr transform(CallExpr.FunctionCallExpr e) {
    Call c = call(e);
    insns.atLine(e.line());
    insns.add(c);
    multis.push(c.dest());
    return e;
  }

  @Override
  public Expr transform(CallExpr.MethodCallExpr e) {
    Call c = call(e);
    insns.atLine(e.line());
    insns.add(c);
    multis.push(c.dest());
    return e;
  }

  private FunctionId nestedFunc(FunctionVarInfo fi, int idx, Block b) {
    IRTranslatorTransformer visitor = new IRTranslatorTransformer(moduleBuilder, id.child(idx));
    visitor.addParamsAndUpvals(fi);
    visitor.mainBlock(b);
    IRFunc result = visitor.result();
    moduleBuilder.add(result);
    return result.id();
  }

  @Override
  public Expr transform(FunctionDefExpr e) {
    FunctionVarInfo info = TranslationUtils.funcVarInfo(e);

    FunctionId id = nestedFunc(info, nextNestedFnIdx++, e.block());

    Val dest = provider.newVal();

    List<AbstractVar> args = new ArrayList<>();
    for (Variable.Ref uv : info.upvalues()) {
//			// FIXME
//			uvs.add(var(uv.var()));

      if (uvs.containsKey(uv)) {
        // it's an upvalue
        args.add(uvs.get(uv));
      } else if (vars.containsKey(uv.var())) {
        // it's a variable
        args.add(vars.get(uv.var()));
      } else {
        throw new IllegalStateException("Illegal upvalue: " + uv);
      }
    }

    insns.atLine(e.line());
    insns.add(new Closure(dest, id, Collections.unmodifiableList(args)));

    vals.push(dest);
    return e;
  }

  @Override
  public Expr transform(TableConstructorExpr e) {
    int array = 0;
    int hash = 0;

    Val dest = provider.newVal();

    for (TableConstructorExpr.FieldInitialiser fi : e.fields()) {
      if (fi.key() == null) {
        array += 1;
      } else {
        hash += 1;
      }
    }

//		vals.push(dest);
    insns.atLine(e.line());
    insns.add(new TabNew(dest, array, hash));

    // According to the Lua 5.3 Reference Manual, the order of assignment is
    // undefined. PUC-Lua appears to first enter all hash entries (with explicit keys)
    // before array fields (with implicit integer fields).

    // We do the same here.

    {
      // hash part
      Iterator<TableConstructorExpr.FieldInitialiser> it = e.fields().iterator();
      while (it.hasNext()) {
        TableConstructorExpr.FieldInitialiser fi = it.next();

        if (fi.key() != null) {
          fi.key().accept(this);
          Val k = popVal();

          fi.value().accept(this);
          Val v = popVal();

          insns.add(new TabRawSet(dest, k, v));
        }
      }
    }

    {
      // array part
      int i = 1;
      Iterator<TableConstructorExpr.FieldInitialiser> it = e.fields().iterator();
      while (it.hasNext()) {
        TableConstructorExpr.FieldInitialiser fi = it.next();

        if (fi.key() == null) {
          Expr ve = fi.value();
          ve.accept(this);

          if (ve instanceof MultiExpr && !it.hasNext() && onStack()) {
            // appending stack contents
            MultiVal mv = multis.pop();
            insns.add(new TabRawAppendMulti(dest, i, mv));
          } else {
            // single value
            Val v = popVal();
            insns.add(new TabRawSetInt(dest, i++, v));
          }
        }
      }
    }

    vals.push(dest);

    return e;
  }

  private void nestedBlock(Block b) {
    for (BodyStatement bs : b.statements()) {
      bs.accept(this);
    }
    if (b.returnStatement() != null) {
      b.returnStatement().accept(this);
    }
  }

  private void mainBlock(Block b) {
    nestedBlock(b);
    if (b.returnStatement() == null) {
      insns.add(new Ret(vlist(Collections.<Expr>emptyList())));
    }
  }

  private void addParamsAndUpvals(FunctionVarInfo fi) {
    for (Variable v : fi.params()) {
      Var w = var(v);
      params.add(w);
    }

    vararg = fi.isVararg();

    for (Variable.Ref uv : fi.upvalues()) {
      UpVar u = upVar(uv);  // FIXME: is this correct?
      upvals.add(u);
    }
  }

  @Override
  public Chunk transform(Chunk chunk) {
    addParamsAndUpvals(TranslationUtils.funcVarInfo(chunk));
    mainBlock(chunk.block());
    moduleBuilder.add(result());
    return chunk;
  }

  @Override
  public ReturnStatement transform(ReturnStatement node) {
    if (node.exprs().size() == 1 && node.exprs().get(0) instanceof CallExpr) {
      // tail call

      CallExpr ce = (CallExpr) node.exprs().get(0);

      final Call c;
      if (ce instanceof CallExpr.FunctionCallExpr) {
        c = call((CallExpr.FunctionCallExpr) ce);
      } else if (ce instanceof CallExpr.MethodCallExpr) {
        c = call((CallExpr.MethodCallExpr) ce);
      } else {
        throw new IllegalStateException("Illegal call expression: " + ce);
      }

      insns.atLine(node.line());
      insns.add(new TCall(c.fn(), c.args()));
    } else {
      VList args = vlist(node.exprs());
      insns.atLine(node.line());
      insns.add(new Ret(args));
    }

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(AssignStatement node) {
    List<Val> ts = new ArrayList<>();

    Iterator<Expr> eit = node.exprs().iterator();
    while (eit.hasNext()) {
      Expr e = eit.next();
      e.accept(this);

      if (e instanceof MultiExpr && !eit.hasNext() && onStack()) {
        // multiple values at tail position
        // -> ignore for now, will be handled below
      } else {
        ts.add(popVal());
      }
    }

    Iterator<Val> it = ts.iterator();

    vals.clear();  // FIXME: needed?

    int i = 0;
    for (LValueExpr lv : node.vars()) {

      final Val src;
      if (it.hasNext()) {
        src = it.next();
      } else {
        src = provider.newVal();
        if (onStack()) {
          insns.add(new MultiGet(src, multis.peek(), i++));
        } else {
          insns.add(new LoadConst.Nil(src));
        }
      }

      vals.push(src);
      assigning = true;
      lv.accept(this);
      assigning = false;
    }

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(LocalDeclStatement node) {
    List<Val> ts = new ArrayList<>();

    Iterator<Expr> eit = node.initialisers().iterator();
    while (eit.hasNext()) {
      Expr e = eit.next();
      e.accept(this);

      if (e instanceof MultiExpr && !eit.hasNext() && onStack()) {
        // multiple values at tail position
        // -> ignore for now, will be handled below
      } else {
        ts.add(popVal());
      }
    }

    Iterator<Val> it = ts.iterator();

    int i = 0;
    for (Variable w : TranslationUtils.varMapping(node).vars()) {
      Var v = var(w);

      final Val src;
      if (it.hasNext()) {
        src = it.next();
      } else {
        src = provider.newVal();
        if (onStack()) {
          insns.add(new MultiGet(src, multis.peek(), i++));
        } else {
          insns.add(new LoadConst.Nil(src));
        }
      }

      insns.add(new VarInit(v, src));
    }

    endStatement();
    return node;
  }

  private void condBlock(ConditionalBlock cb, Label l_else, Label l_done) {
    Objects.requireNonNull(l_done);

    insns.atLine(cb.condition().line());

    cb.condition().accept(this);
    Val c = popVal();

    insns.addBranch(new Branch.Condition.Bool(c, false), l_else != null ? l_else : l_done);
    nestedBlock(cb.block());

    if (l_else != null && insns.isInBlock()) {
      insns.add(new Jmp(l_done));
    }
  }

  private Label nextLabel(Iterator<Label> ls) {
    return ls.hasNext() ? ls.next() : null;
  }

  @Override
  public BodyStatement transform(IfStatement node) {
    Label l_done = insns.newLabel();

    List<Label> nexts = new ArrayList<>();
    for (ConditionalBlock cb : node.elifs()) {
      nexts.add(insns.newLabel());
    }
    if (node.elseBlock() != null) {
      nexts.add(insns.newLabel());
    }

    Iterator<Label> ls = nexts.iterator();

    Label l_next = nextLabel(ls);
    condBlock(node.main(), l_next, l_done);
    for (ConditionalBlock cb : node.elifs()) {
      assert (l_next != null);
      insns.add(l_next);
      l_next = nextLabel(ls);

      condBlock(cb, l_next, l_done);
    }

    if (node.elseBlock() != null) {
      assert (l_next != null);
      insns.add(l_next);
      nestedBlock(node.elseBlock());
    }

    insns.add(l_done);

    endStatement();
    return node;
  }

  private Val toNumber(Val addr, String desc) {
    Val t = provider.newVal();
    insns.add(new ToNumber(t, addr, desc));
    return t;
  }

  private Val loadConst(int i) {
    Val t = provider.newVal();
    insns.add(new LoadConst.Int(t, i));
    return t;
  }

  @Override
  public BodyStatement transform(NumericForStatement node) {
    Label l_top = insns.newLabel();
    Label l_done = insns.newLabel();

    insns.atLine(node.line());
    // TODO: any more lines to insert?

    // Note: we coerce parameters to numbers in the same order as in PUC Lua to get
    // the same error reporting.

    node.limit().accept(this);
    Val t_limit = toNumber(popVal(), "'for' limit");

    final Val t_step;
    if (node.step() != null) {
      node.step().accept(this);
      t_step = toNumber(popVal(), "'for' step");
    } else {
      t_step = loadConst(1);
    }

    node.init().accept(this);
    Val t_var0 = toNumber(popVal(), "'for' initial value");

    // var = var - step
    Val t_var1 = provider.newVal();
    insns.add(new BinOp(BinOp.Op.SUB, t_var1, t_var0, t_step));

    Var v_var = var(new Variable(node.name()));  // FIXME
    insns.add(new VarInit(v_var, t_var1));

    insns.add(l_top);

    Val t_var2 = provider.newVal();
    insns.add(new VarLoad(t_var2, v_var));

    Val t_var3 = provider.newVal();
    insns.add(new BinOp(BinOp.Op.ADD, t_var3, t_var2, t_step));

    // check end-condition
    insns.addBranch(new Branch.Condition.NumLoopEnd(t_var3, t_limit, t_step), l_done);

    insns.add(new VarStore(v_var, t_var3));

    VarMapping vm = TranslationUtils.varMapping(node);
    Var v_v = var(vm.get());
    insns.add(new VarInit(v_v, t_var3));

    breakLabels.push(l_done);
    nestedBlock(node.block());
    breakLabels.pop();

    insns.add(new Jmp(l_top));

    insns.add(l_done);

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(GenericForStatement node) {
    Label l_top = insns.newLabel();
    Label l_done = insns.newLabel();

    insns.atLine(node.line());
    // TODO: any more lines to insert?

    VarMapping vm = TranslationUtils.varMapping(node);

    Val t_f = provider.newVal();
    Val t_s = provider.newVal();
    Val t_var0 = provider.newVal();

    Var v_var = provider.newVar();  // FIXME

    // local f, s, var = explist
    {
      int i = 0;
      Iterator<Expr> eit = node.exprs().iterator();
      while (eit.hasNext()) {
        Expr e = eit.next();
        e.accept(this);

        if (e instanceof MultiExpr && !eit.hasNext() && onStack()) {
          // multiple values at tail position
          // -> ignore, will be dealt with below
        } else {
          // single value
          switch (i) {
            case 0:
              t_f = popVal();
              break;
            case 1:
              t_s = popVal();
              break;
            case 2:
              t_var0 = popVal();
              break;
            default:
              popVal();
              break;  // discard result
          }
          i += 1;
        }

      }

      if (onStack()) {
        // fill in the rest from the stack; note that the cases fall through
        MultiVal mv = multis.pop();
        switch (i) {
          case 0:
            insns.add(new MultiGet(t_f, mv, 0)); // fall through
          case 1:
            insns.add(new MultiGet(t_s, mv, 1)); // fall through
          case 2:
            insns.add(new MultiGet(t_var0, mv, 2)); // fall through
          default:
        }
      } else {
        // pad with nils if necessary; note that the cases fall through
        switch (i) {
          case 0:
            insns.add(new LoadConst.Nil(t_f)); // fall through
          case 1:
            insns.add(new LoadConst.Nil(t_s)); // fall through
          case 2:
            insns.add(new LoadConst.Nil(t_var0)); // fall through
          default:
        }
      }

      multis.clear();  // FIXME
//			onStack = false;

      insns.add(new VarInit(v_var, t_var0));
    }

    insns.add(l_top);

    Val t_var1 = provider.newVal();
    insns.add(new VarLoad(t_var1, v_var));

    List<Val> ts = new ArrayList<>();
    ts.add(t_s);
    ts.add(t_var1);
    MultiVal mv = provider.newMultiVal();
    insns.add(new Call(mv, t_f, new VList(Collections.unmodifiableList(ts), null)));

    for (int i = 0; i < node.names().size(); i++) {
      Var v = var(vm.get(i));
      Val t = provider.newVal();
      insns.add(new MultiGet(t, mv, i));
      insns.add(new VarInit(v, t));
    }

    Val t_v1 = provider.newVal();
    insns.add(new VarLoad(t_v1, var(vm.get(0))));

    insns.addBranch(new Branch.Condition.Nil(t_v1), l_done);

    insns.add(new VarStore(v_var, t_v1));

    breakLabels.push(l_done);
    nestedBlock(node.block());
    breakLabels.pop();

    insns.add(new Jmp(l_top));

    insns.add(l_done);

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(WhileStatement node) {
    Label l_test = insns.newLabel();
    Label l_done = insns.newLabel();

    insns.atLine(node.line());
    // TODO: any more lines to insert?

    insns.add(l_test);
    node.condition().accept(this);
    Val c = popVal();
    insns.addBranch(new Branch.Condition.Bool(c, false), l_done);

    breakLabels.push(l_done);
    nestedBlock(node.block());
    breakLabels.pop();

    insns.add(new Jmp(l_test));

    insns.add(l_done);

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(RepeatUntilStatement node) {
    Label l_body = insns.newLabel();
    Label l_done = insns.newLabel();

    insns.atLine(node.line());
    // TODO: any more lines to insert?

    insns.add(l_body);
    breakLabels.push(l_done);
    nestedBlock(node.block());
    breakLabels.pop();

    node.condition().accept(this);
    Val c = popVal();
    insns.addBranch(new Branch.Condition.Bool(c, true), l_done);

    insns.add(new Jmp(l_body));

    insns.add(l_done);

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(LabelStatement node) {
    ResolvedLabel rl = TranslationUtils.resolvedLabel(node);

    insns.add(userLabel(rl));
    insns.atLine(node.line());

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(GotoStatement node) {
    ResolvedLabel rl = TranslationUtils.resolvedLabel(node);

    insns.atLine(node.line());
    insns.add(new Jmp(userLabel(rl)));

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(BreakStatement node) {
    insns.atLine(node.line());

    if (breakLabels.isEmpty()) {
      throw new IllegalStateException(
          "<break> at " + AttributeUtils.sourceInfoString(node) + " not inside a loop");
    } else {
      Label l = breakLabels.peek();
      insns.add(new Jmp(l));
    }

    endStatement();
    return node;
  }

  @Override
  public BodyStatement transform(CallStatement node) {
    insns.atLine(node.line());

    node.callExpr().accept(this);

    // results are discarded

    endStatement();
    return node;
  }

}
