// Loops, and the labels wap generates so you do not have to.
//
// A Wax loop needs a name to branch to; a wap loop does not. What follows is
// the whole of that difference: a frame per enclosing loop, a label minted the
// first time something jumps to it, and no enclosing block emitted when nothing
// breaks out.

///|
/// Push a loop frame and return the label a `continue` will use.
fn Lowering::enter_loop(
  self : Lowering,
  name : String?,
  stem : String,
) -> LoopFrame {
  let continue_label = match name {
    Some(n) => n
    None => self.gensym(stem)
  }
  let frame : LoopFrame = {
    name,
    continue_label,
    break_label: None,
    break_used: false,
    continue_used: false,
  }
  self.loops.push(frame)
  frame
}

///|
fn Lowering::leave_loop(self : Lowering) -> Unit {
  let _ = self.loops.pop()
}

///|
/// The frame a `break`/`continue` refers to.
fn Lowering::frame_for(self : Lowering, name : String?) -> LoopFrame? {
  match name {
    None =>
      if self.loops.length() == 0 {
        None
      } else {
        Some(self.loops[self.loops.length() - 1])
      }
    Some(n) => {
      for i = self.loops.length() - 1; i >= 0; i = i - 1 {
        if self.loops[i].name == Some(n) {
          return Some(self.loops[i])
        }
      }
      None
    }
  }
}

///|
/// `break` and `continue`, which are both a `br` once the label exists.
fn Lowering::jump(
  self : Lowering,
  name : String?,
  is_break : Bool,
  sp : @wap.Span,
) -> @ast.Instr[@basic.Location] {
  let at = self.loc(sp)
  match self.frame_for(name) {
    None => {
      self.error(
        if is_break {
          "`break` is not inside a loop"
        } else {
          "`continue` is not inside a loop"
        },
        sp,
      )
      @ast.build(Unreachable, at)
    }
    Some(f) =>
      if is_break {
        let label = match f.break_label {
          Some(l) => l
          None => {
            let l = self.gensym(f.continue_label + "__done")
            f.break_label = Some(l)
            l
          }
        }
        f.break_used = true
        @ast.build(Br(self.ident(label, sp), None), at)
      } else {
        f.continue_used = true
        @ast.build(Br(self.ident(f.continue_label, sp), None), at)
      }
  }
}

///|
/// Wrap a loop in the block a `break` branches out of, but only if one did.
fn Lowering::wrap_break(
  self : Lowering,
  f : LoopFrame,
  inner : @ast.Instr[@basic.Location],
  sp : @wap.Span,
) -> @ast.Instr[@basic.Location] {
  if !f.break_used {
    return inner
  }
  let at = self.loc(sp)
  let label = match f.break_label {
    Some(l) => l
    None => self.gensym("done")
  }
  @ast.build(
    Block(label=Some(self.ident(label, sp)), typ=empty_type(), block={
      desc: [inner],
      info: at,
    }),
    at,
  )
}

///|
/// `while c ~step(s): body`
fn Lowering::while_loop(
  self : Lowering,
  label : String?,
  cond : @wap.Node,
  step : @wap.Node?,
  body : Array[@wap.Node],
  sp : @wap.Span,
) -> @ast.Instr[@basic.Location] {
  let at = self.loc(sp)
  let f = self.enter_loop(label, "while")
  self.push_scope()
  let block = self.blk(body, sp)
  let c = self.expr(cond, None)
  let s = match step {
    Some(s) => Some(self.expr(s, None))
    None => None
  }
  self.pop_scope()
  self.leave_loop()
  // The loop's own label is emitted only when a `continue` used it.
  let lbl = if self.label_used(f) {
    Some(self.ident(f.continue_label, sp))
  } else {
    None
  }
  let w = @ast.build(While(label=lbl, cond=c, step=s, block~), at)
  self.wrap_break(f, w, sp)
}

///|
/// Whether the loop's own label was branched to.
///
/// A `continue` mints nothing -- the label already exists -- so this asks
/// whether the label was named, which the frame records by having been used at
/// all. Naming it always would be correct and would also emit labels nothing
/// refers to, which the printed expansion should not contain.
fn Lowering::label_used(self : Lowering, f : LoopFrame) -> Bool {
  ignore(self)
  f.name is Some(_) || f.break_used || f.continue_used
}

///|
/// `loop: body`
fn Lowering::plain_loop(
  self : Lowering,
  label : String?,
  body : Array[@wap.Node],
  sp : @wap.Span,
) -> @ast.Instr[@basic.Location] {
  let at = self.loc(sp)
  let f = self.enter_loop(label, "loop")
  self.push_scope()
  let block = self.blk(body, sp)
  self.pop_scope()
  // A wasm `loop` is a branch target, not a repetition: it runs once unless
  // something branches back to it. Wax makes you write that `br`; wap writes
  // it, which is the only reason `loop:` means what it looks like.
  block.desc.push(@ast.build(Br(self.ident(f.continue_label, sp), None), at))
  self.leave_loop()
  let l = @ast.build(
    Loop(label=Some(self.ident(f.continue_label, sp)), typ=empty_type(), block~),
    at,
  )
  self.wrap_break(f, l, sp)
}

///|
/// `for i in a .. b by n: body`
fn Lowering::for_range(
  self : Lowering,
  label : String?,
  var_ : String,
  from : @wap.Node,
  to : @wap.Node,
  inclusive : Bool,
  by : @wap.Node?,
  body : Array[@wap.Node],
  sp : @wap.Span,
) -> Array[@ast.Instr[@basic.Location]] {
  let at = self.loc(sp)
  let ity = match self.type_of(from) {
    Some(t) => Some(t)
    None => self.type_of(to)
  }
  self.push_scope()
  match ity {
    Some(t) => self.bind(var_, t)
    None => self.bind(var_, I32)
  }
  let out = []
  // The counter.
  out.push(
    @ast.build(
      Let([(Some(self.ident(var_, sp)), None)], Some(self.expr(from, ity))),
      at,
    ),
  )
  // The limit is evaluated once, so a call in it is not re-run every step.
  let limit : @wap.Node = match to.it {
    Int(_) | Var(_) => to
    _ => {
      let n = self.gensym("n")
      match ity {
        Some(t) => self.bind(n, t)
        None => self.bind(n, I32)
      }
      out.push(
        @ast.build(
          Let(
            [(Some({ name: n, loc: self.fresh_loc(), }), None)],
            Some(self.expr(to, ity)),
          ),
          at,
        ),
      )
      { it: Var(n), span: sp, }
    }
  }
  let subject : @wap.Node = { it: Var(var_), span: sp, }
  let cond : @wap.Node = {
    it: Bin(if inclusive { Le } else { Lt }, subject, limit),
    span: sp,
  }
  let stride = match by {
    Some(b) => b
    None => ({ it: Int("1"), span: sp, } : @wap.Node)
  }
  let step : @wap.Node = {
    it: Assign(targets=[subject], op=OpSet(Add), value=stride),
    span: sp,
  }
  let f = self.enter_loop(label, "for")
  let block = self.blk(body, sp)
  let c = self.expr(cond, None)
  let s = self.expr(step, None)
  self.leave_loop()
  let lbl = if self.label_used(f) {
    Some(self.ident(f.continue_label, sp))
  } else {
    None
  }
  let w = @ast.build(While(label=lbl, cond=c, step=Some(s), block~), at)
  out.push(self.wrap_break(f, w, sp))
  self.pop_scope()
  out
}

///|
/// `for x in xs: body`
fn Lowering::for_in(
  self : Lowering,
  label : String?,
  var_ : String,
  seq : @wap.Node,
  body : Array[@wap.Node],
  sp : @wap.Span,
) -> Array[@ast.Instr[@basic.Location]] {
  let at = self.loc(sp)
  let stype = self.type_of(seq)
  let elem = match stype {
    Some(t) => self.elem_type(t)
    None => None
  }
  self.push_scope()
  let out = []
  // The sequence, bound once.
  let holder : @wap.Node = match seq.it {
    Var(_) => seq
    _ => {
      let s = self.gensym("seq")
      match stype {
        Some(t) => self.bind(s, t)
        None => ()
      }
      out.push(
        @ast.build(
          Let(
            [(Some({ name: s, loc: self.fresh_loc(), }), None)],
            Some(self.expr(seq, None)),
          ),
          at,
        ),
      )
      { it: Var(s), span: sp, }
    }
  }
  let idx = self.gensym("i")
  let len = self.gensym("n")
  self.bind(idx, U32)
  self.bind(len, U32)
  out.push(
    @ast.build(
      Let(
        [(Some({ name: idx, loc: self.fresh_loc(), }), None)],
        Some(@ast.build(Int("0"), at)),
      ),
      at,
    ),
  )
  out.push(
    @ast.build(
      Let(
        [(Some({ name: len, loc: self.fresh_loc(), }), None)],
        Some(
          @ast.build(
            Call(
              @ast.build(
                StructGet(self.expr(holder, None), self.ident("length", sp)),
                at,
              ),
              [],
            ),
            at,
          ),
        ),
      ),
      at,
    ),
  )
  let idx_node : @wap.Node = { it: Var(idx), span: sp, }
  let cond : @wap.Node = {
    it: Bin(Lt, idx_node, { it: Var(len), span: sp, }),
    span: sp,
  }
  let step : @wap.Node = {
    it: Assign(targets=[idx_node], op=OpSet(Add), value={
      it: Int("1"),
      span: sp,
    }),
    span: sp,
  }
  let f = self.enter_loop(label, "for")
  self.push_scope()
  match elem {
    Some(t) => self.bind(var_, t)
    None => ()
  }
  // The element, bound at the top of the body.
  let inner = [
    @ast.build(
      Let(
        [(Some(self.ident(var_, sp)), None)],
        Some(
          self.widen(
            @ast.build(
              ArrayGet(self.expr(holder, None), self.expr(idx_node, None)),
              at,
            ),
            elem,
            sp,
          ),
        ),
      ),
      at,
    ),
  ]
  for b in self.body(body) {
    inner.push(b)
  }
  self.pop_scope()
  let c = self.expr(cond, None)
  let s = self.expr(step, None)
  self.leave_loop()
  let lbl = if self.label_used(f) {
    Some(self.ident(f.continue_label, sp))
  } else {
    None
  }
  let w = @ast.build(
    While(label=lbl, cond=c, step=Some(s), block={ desc: inner, info: at, }),
    at,
  )
  out.push(self.wrap_break(f, w, sp))
  self.pop_scope()
  out
}