///|
fn Plan::start(
  self : Plan,
  id : Int32,
  input : Value,
  env : Env,
  tracking : Bool,
) -> Machine raise EvalError {
  let node = self.nodes[id]
  let span = node.span
  fn run(n : Int32) -> Machine {
    self.machine(n, input, env, tracking)
  }
  fn values(n : Int32) -> Machine {
    self.machine(n, input, env, false)
  }
  match node.code {
    Identity => Machine::one(input)
    Constant(json) => Machine::one(value(json))
    Pipe(left, right) =>
      run(left).flat_map(v => self.machine(right, v, env, tracking))
    Comma(left, right) => run(left).concat(run(right))
    Navigate(base, Lookup(key)) =>
      values(key).flat_map(k => {
        run(base).map(v => access(v, k.json, tracking, span))
      })
    Navigate(base, Each) => run(base).flat_map(v => each(v, tracking, span))
    Navigate(base, Slice(a, b)) =>
      run(base).map(v => {
        check_access(v, tracking, span)
        if !(v.json is (Array(_) | String(_) | Null)) {
          raise EvalError(
            Json::string("Cannot slice " + @ast_internal.json_type_name(v.json)),
            span,
          )
        }
        access(v, slice_key(a, b), tracking, span)
      })
    Collect(None) => Machine::one(value(Json::array([])))
    Collect(Some(body)) =>
      values(body).collect(vs => {
        Machine::one(value(Json::array(vs.map(v => v.json))))
      })
    Object(fields) => self.object(fields, 0, Map([]), input, env, span)
    Binary(left, And, right) =>
      values(left).flat_map(a => {
        if @ast_internal.is_truthy(a.json) {
          values(right).map(b => {
            value(Json::boolean(@ast_internal.is_truthy(b.json)))
          })
        } else {
          Machine::one(value(Json::boolean(false)))
        }
      })
    Binary(left, Or, right) =>
      values(left).flat_map(a => {
        if @ast_internal.is_truthy(a.json) {
          Machine::one(value(Json::boolean(true)))
        } else {
          values(right).map(b => {
            value(Json::boolean(@ast_internal.is_truthy(b.json)))
          })
        }
      })
    Binary(left, op, right) =>
      values(right).flat_map(b => {
        values(left).map(a => {
          let json = eval_binary_op(op, a.json, b.json) catch {
            err => raise primitive_error(err, span)
          }
          value(json)
        })
      })
    If(cond, yes, no) =>
      values(cond).flat_map(c => {
        run(if @ast_internal.is_truthy(c.json) { yes } else { no })
      })
    Catch(body, handler) =>
      Machine::new(
        Catch(run(body), err => {
          match handler {
            Some(h) => self.machine(h, value(err.value()), env, tracking)
            None => Machine::empty()
          }
        }),
      )
    Alternative(left, right) =>
      Machine::new(Alternative(run(left), false, fn() { run(right) }))
    Variable(binding) =>
      match env.values.get(binding) {
        Some(Bound(v)) => Machine::one(value(v))
        _ => Machine::fail(EvalError(Json::string("Unbound variable"), span))
      }
    Bind(source, binding, body) =>
      values(source).flat_map(v => {
        self.machine(body, input, env.bind(binding, Bound(v.json)), tracking)
      })
    Scope(function, body) => {
      let functions = env.functions.copy()
      functions[function] = env
      self.machine(body, input, { values: env.values, functions, }, tracking)
    }
    Filter(binding) =>
      match env.values.get(binding) {
        Some(Closure(body, caller)) =>
          self.machine(body, input, caller, tracking)
        _ => Machine::fail(EvalError(Json::string("Unbound filter"), span))
      }
    Call(function, args) => {
      let captured = env.functions.get(function).unwrap_or(env)
      let functions = captured.functions.copy()
      functions[function] = captured
      let mut callee : Env = { values: captured.values, functions, }
      for i, binding in self.functions[function].params {
        callee = callee.bind(binding, Closure(args[i], env))
      }
      self.machine(self.functions[function].body, input, callee, tracking)
    }
    Builtin(name, args) => self.builtin(name, args, input, env, tracking, span)
    ValueBuiltin(name, bindings) => {
      let args = bindings.map(id => {
        match env.values.get(id) {
          Some(Bound(json)) => json
          _ => raise EvalError(Json::string("Unbound builtin argument"), span)
        }
      })
      self.value_builtin(name, args, input, tracking, span)
    }
    Reduce(gen, binding, init, update, extract) =>
      values(init).flat_map(initial => {
        self.reduce_stream(
          values(gen),
          binding,
          initial,
          update,
          extract,
          env,
          span,
        )
      })
    Update(path, body, mode) => self.update(path, body, mode, input, env, span)
  }
}

///|
fn Plan::object(
  self : Plan,
  fields : Array[(Int32, Int32?)],
  index : Int32,
  object : Map[String, Json],
  input : Value,
  env : Env,
  span : @ast.Span?,
) -> Machine {
  if index == fields.length() {
    return Machine::one(value(Json::object(object)))
  }
  let (key, body) = fields[index]
  self
  .machine(key, input, env, false)
  .flat_map(k => {
    guard k.json is String(name) else {
      raise EvalError(Json::string("Object keys must be strings"), span)
    }
    let values = match body {
      Some(n) => self.machine(n, input, env, false)
      None => Machine::one(access(input, k.json, false, span))
    }
    values.flat_map(v => {
      let next = object.copy()
      next[name] = v.json
      self.object(fields, index + 1, next, input, env, span)
    })
  })
}