///|
priv struct SourceCursor {
  source : MatSource
  producer : SourceInflater?
  buffer : FixedArray[Byte]
  mut position : Int
  mut start : Int
  mut available : Int
}

///|
#inline
fn SourceCursor::byte(self : SourceCursor) -> Byte raise MatError {
  if self.position < self.start || self.position >= self.start + self.available {
    if self.position >= self.source.size {
      fail("truncated-data", self.position, "Unexpected source end")
    }
    self.start = self.position
    self.available = if self.source.size - self.position < self.buffer.length() {
      self.source.size - self.position
    } else {
      self.buffer.length()
    }
    match self.producer {
      None =>
        source_fill(
          self.source,
          self.position,
          self.buffer,
          0,
          self.available,
          self.buffer.length(),
        )
      Some(producer) => producer.fill(self.buffer, self.available)
    }
  }
  let value = self.buffer[self.position - self.start]
  self.position = self.position + 1
  value
}

///|
fn SourceCursor::owned(
  self : SourceCursor,
  count : Int,
) -> Bytes raise MatError {
  if count < 0 || count > self.source.size - self.position {
    fail("truncated-data", self.position, "Payload exceeds source")
  }
  let out = FixedArray::make(count, b'\x00')
  let mut done = 0
  while done < count {
    out[done] = self.byte()
    done = done + 1
    let available = self.start + self.available - self.position
    let n = if available < count - done { available } else { count - done }
    self.buffer.blit_to(
      out,
      len=n,
      src_offset=self.position - self.start,
      dst_offset=done,
    )
    self.position = self.position + n
    done = done + n
  }
  finish_owned_buffer(out)
}

///|
fn SourceCursor::skip(self : SourceCursor, count : Int) -> Unit raise MatError {
  if count < 0 || count > self.source.size - self.position {
    fail("truncated-data", self.position, "Payload exceeds source")
  }
  let end = self.position + count
  while self.position < end {
    ignore(self.byte())
    let available = self.start + self.available - self.position
    self.position = self.position +
      (if available > end - self.position {
        end - self.position
      } else {
        available
      })
  }
}

///|
priv struct SourceTag {
  kind : Int
  size : Int
  start : Int
  next : Int
  inline_data : Bytes?
}

///|
fn SourceCursor::tag(
  self : SourceCursor,
  end : Int,
  little : Bool,
) -> SourceTag raise MatError {
  let offset = self.position
  if end - offset < 8 {
    fail("truncated-tag", offset, "Element tag needs eight bytes")
  }
  let raw = self.owned(8)
  let word = bits_at(raw, 0, 4, little).to_uint()
  let small = (word >> 16).reinterpret_as_int()
  if small != 0 {
    let kind = (word & 65535U).reinterpret_as_int()
    if small > 4 || kind == 0 || kind == 14 || kind == 15 {
      fail("invalid-small-tag", offset, "Invalid inline element")
    }
    return {
      kind,
      size: small,
      start: offset + 4,
      next: offset + 8,
      inline_data: Some(raw[4:4 + small].to_owned()),
    }
  }
  let kind = word.reinterpret_as_int()
  let size = bits_at(raw, 4, 4, little)
  if size > (end - offset - 8).to_uint64() {
    fail("truncated-data", offset, "Element exceeds enclosing matrix")
  }
  let n = size.to_int()
  let padding = if kind == 15 { 0 } else { (8 - n % 8) % 8 }
  if padding > end - offset - 8 - n {
    fail("truncated-padding", offset, "Missing element padding")
  }
  {
    kind,
    size: n,
    start: self.position,
    next: self.position + n + padding,
    inline_data: None,
  }
}

///|
fn SourceCursor::payload(
  self : SourceCursor,
  tag : SourceTag,
) -> Bytes raise MatError {
  let out = match tag.inline_data {
    Some(b) => b
    None => self.owned(tag.size)
  }
  self.finish(tag)
  out
}

///|
fn SourceCursor::finish(
  self : SourceCursor,
  tag : SourceTag,
) -> Unit raise MatError {
  self.skip(tag.next - self.position)
}

///|
priv struct SourceScan {
  limits : SourceLimits
  basic : Limits
  tree : TreeLimits
  budget : TreeBudget
  wanted : Map[String, Bool]?
  mut result_size : Int
  mut expanded : Int
}

///|
fn SourceScan::metadata(self : SourceScan, count : Int) -> Unit raise MatError {
  if count > self.limits.max_metadata_bytes - self.budget.metadata {
    fail("metadata-limit", 0, "Cumulative source metadata exceeds quota")
  }
  self.budget.metadata = self.budget.metadata + count
}

///|
fn SourceScan::result(
  self : SourceScan,
  count : Int,
  keep : Bool,
) -> Unit raise MatError {
  if !keep {
    return
  }
  if count < 0 || count > self.limits.max_result_bytes - self.result_size {
    fail("result-limit", 0, "Selected normalized MAT exceeds quota")
  }
  self.result_size = self.result_size + count
}

///|
fn source_text_value(
  raw : Bytes,
  limits : Limits,
  empty : Bool,
) -> String raise MatError {
  tree_text(
    raw,
    { kind: 1, start: 0, size: raw.length(), next: raw.length(), },
    limits,
    empty,
  )
}

///|
fn source_plane(
  cursor : SourceCursor,
  tag : SourceTag,
  count : Int,
  dtype : DType,
  little : Bool,
  keep : Bool,
) -> Bytes raise MatError {
  let storage = from_mi_type(tag.kind, tag.start)
  if count > 1073741824 / storage.width() || tag.size != count * storage.width() {
    fail("payload-size", tag.start, "Numeric plane size differs from shape")
  }
  if !keep && (storage == dtype || dtype == Logical) {
    cursor.finish(tag)
    return b""
  }
  if storage == dtype && little {
    let bytes = cursor.payload(tag)
    return bytes
  }
  let out = FixedArray::make(
    if keep {
      count * dtype.width()
    } else {
      0
    },
    b'\x00',
  )
  let mut done = 0
  let block_elements = if cursor.buffer.length() / storage.width() > 0 {
    cursor.buffer.length() / storage.width()
  } else {
    1
  }
  while done < count {
    let n = if count - done > block_elements {
      block_elements
    } else {
      count - done
    }
    let bytes = match tag.inline_data {
      Some(b) =>
        b[done * storage.width():(done + n) * storage.width()].to_owned()
      None => cursor.owned(n * storage.width())
    }
    let converted = numeric_payload(
      bytes,
      { kind: tag.kind, start: 0, size: bytes.length(), next: bytes.length(), },
      dtype,
      n,
      little,
      keep~,
    )
    if keep {
      out.blit_from_bytes(
        done * dtype.width(),
        converted,
        0,
        converted.length(),
      )
    }
    done = done + n
  }
  cursor.finish(tag)
  finish_owned_buffer(out)
}

///|
fn source_indices(
  cursor : SourceCursor,
  tag : SourceTag,
  keep : Bool,
  row_bound : Int,
  slots : Int,
  pointers : Bool,
  little : Bool,
) -> (Array[Int], Int) raise MatError {
  let count = tag.size / 4
  let out = if keep { Array::make(count, 0) } else { [] }
  let mut first_bad = -1
  let mut last = 0
  for i = 0; i < count; i = i + 1 {
    let data = match tag.inline_data {
      Some(b) => b[i * 4:i * 4 + 4].to_owned()
      None => cursor.owned(4)
    }
    let value = csc_index_at(data, 0, little)
    if pointers {
      if (i == 0 && value != 0) || value < last || value > slots {
        fail("invalid-sparse", tag.start + i * 4, "Invalid CSC pointer")
      }
      last = value
    } else if first_bad < 0 && (value < 0 || value >= row_bound) {
      first_bad = i
    }
    if keep {
      out[i] = value
    }
  }
  cursor.finish(tag)
  (out, if pointers { last } else { first_bad })
}

///|
fn source_character(
  cursor : SourceCursor,
  tag : SourceTag,
  encoding : CharEncoding,
  count : Int,
  little : Bool,
  keep : Bool,
) -> Bytes raise MatError {
  if keep {
    let bytes = cursor.payload(tag)
    validate_char_units(bytes[:], encoding, count, little)
    let width = encoding.width()
    return if little || width == 1 {
      bytes
    } else {
      Bytes::makei(bytes.length(), i => {
        bytes[i / width * width + width - 1 - i % width]
      })
    }
  }
  if encoding != Utf8 {
    if count > 1073741824 / encoding.width() ||
      tag.size != count * encoding.width() {
      fail("payload-size", tag.start, "Character unit count differs from shape")
    }
    if encoding == Utf32 {
      for i = 0; i < count; i = i + 1 {
        let bytes = match tag.inline_data {
          Some(b) => b[i * 4:i * 4 + 4].to_owned()
          None => cursor.owned(4)
        }
        let cp = bits_at(bytes, 0, 4, little)
        if cp > 1114111UL || (cp >= 55296UL && cp <= 57343UL) {
          fail("invalid-char", tag.start + i * 4, "Invalid UTF32 scalar")
        }
      }
    }
    cursor.finish(tag)
    return b""
  }
  let mut remaining = 0
  let mut cp = 0
  let mut minimum = 0
  let mut scalars = 0
  for i = 0; i < tag.size; i = i + 1 {
    let byte = match tag.inline_data {
      Some(b) => b[i].to_int()
      None => cursor.byte().to_int()
    }
    if remaining == 0 {
      if byte < 128 {
        scalars = scalars + 1
      } else if byte >= 194 && byte <= 223 {
        remaining = 1
        cp = byte & 31
        minimum = 128
      } else if byte >= 224 && byte <= 239 {
        remaining = 2
        cp = byte & 15
        minimum = 2048
      } else if byte >= 240 && byte <= 244 {
        remaining = 3
        cp = byte & 7
        minimum = 65536
      } else {
        fail("invalid-char", tag.start + i, "Invalid UTF8 leading byte")
      }
    } else {
      if (byte & 192) != 128 {
        fail("invalid-char", tag.start + i, "Invalid UTF8 continuation")
      }
      cp = (cp << 6) | (byte & 63)
      remaining = remaining - 1
      if remaining == 0 {
        if cp < minimum || cp > 1114111 || (cp >= 55296 && cp <= 57343) {
          fail("invalid-char", tag.start + i, "Invalid UTF8 scalar")
        }
        scalars = scalars + 1
      }
    }
  }
  if remaining != 0 {
    fail("invalid-char", tag.start, "Truncated UTF8 scalar")
  }
  if scalars != count {
    fail("payload-size", tag.start, "UTF8 scalar count differs from dimensions")
  }
  cursor.finish(tag)
  b""
}

///|
fn source_stream_node(
  cursor : SourceCursor,
  outer : SourceTag,
  little : Bool,
  state : SourceScan,
  depth : Int,
  keep_parent : Bool,
) -> (MatNode?, MatRootInfo) raise MatError {
  charge_node(state.budget, depth, state.tree)
  if outer.kind != 14 {
    fail("invalid-child", outer.start, "Expected matrix child")
  }
  if outer.size == 0 && depth > 1 {
    state.result(56, keep_parent)
    return (
      if keep_parent {
        Some(
          Numeric({
            name: "",
            dtype: Float64,
            shape: [0, 0],
            real: b"",
            imag: None,
            is_global: false,
          }),
        )
      } else {
        None
      },
      {
        name: "",
        class_id: 6,
        shape: [0, 0],
        is_global: false,
        is_complex: false,
        is_logical: false,
        compressed: false,
      },
    )
  }
  let end = outer.start + outer.size
  let flags = cursor.tag(end, little)
  if flags.kind != 6 || flags.size != 8 {
    fail("invalid-flags", flags.start, "Expected array flags")
  }
  let flags_data = cursor.payload(flags)
  let bits = bits_at(flags_data, 0, 4, little).to_uint()
  let reserved = bits_at(flags_data, 4, 4, little)
  let class = (bits & 255U).reinterpret_as_int()
  if class < 1 || class > 15 {
    fail("unsupported-class", flags.start, "Unsupported base class")
  }
  let allowed = if class <= 4 { 0xfffffb00U } else { 0xfffff100U }
  if (bits & allowed) != 0U || (class != 5 && reserved > 1UL) {
    fail("unsupported-flags", flags.start, "Unknown flags or reserved metadata")
  }
  let complex = (bits & 2048U) != 0U
  let logical = (bits & 512U) != 0U
  let dtype = if logical {
    Logical
  } else if class == 5 {
    Float64
  } else if class >= 6 {
    from_mat_class(class, flags.start)
  } else {
    Float64
  }
  if complex && dtype != Float64 && dtype != Float32 {
    fail(
      "unsupported-complex",
      flags.start,
      "Complex storage needs floating data",
    )
  }
  let dims = cursor.tag(end, little)
  if dims.kind != 5 ||
    dims.size < 8 ||
    dims.size % 4 != 0 ||
    dims.size / 4 > state.basic.max_dimensions {
    fail("invalid-shape", dims.start, "Invalid dimension vector")
  }
  state.metadata(dims.size)
  let data = cursor.payload(dims)
  let shape = Array::makei(data.length() / 4, i => {
    csc_index_at(data, i * 4, little)
  })
  if class == 5 &&
    (
      shape.length() != 2 ||
      shape[0] < 0 ||
      shape[1] < 0 ||
      shape[1] >= state.basic.max_elements
    ) {
    fail("invalid-shape", dims.start, "Invalid CSC shape")
  }
  let count = if class == 5 {
    0
  } else {
    element_count(shape, state.basic, dims.start)
  }
  if class != 5 {
    charge_elements(state.budget, count, state.basic)
  }
  let name_tag = cursor.tag(end, little)
  if name_tag.kind != 1 || name_tag.size > state.basic.max_name_bytes {
    fail("invalid-name", name_tag.start, "Invalid matrix name")
  }
  state.metadata(name_tag.size)
  let name = source_text_value(cursor.payload(name_tag), state.basic, depth > 1)
  let keep = if depth == 1 {
    match state.wanted {
      None => true
      Some(w) => w.contains(name)
    }
  } else {
    keep_parent
  }
  let info : MatRootInfo = {
    name,
    class_id: class,
    shape: shape.copy(),
    is_global: (bits & 1024U) != 0U,
    is_complex: complex,
    is_logical: logical,
    compressed: false,
  }
  state.result(
    8 + 16 + element_size(dims.size) + element_size(name_tag.size),
    keep,
  )
  let value : MatNode? = if class >= 6 {
    let real_tag = cursor.tag(end, little)
    state.result(element_size(count * dtype.width()), keep)
    let real = source_plane(cursor, real_tag, count, dtype, little, keep)
    let imag = if complex {
      let tag = cursor.tag(end, little)
      state.result(element_size(count * dtype.width()), keep)
      Some(source_plane(cursor, tag, count, dtype, little, keep))
    } else {
      None
    }
    if keep {
      Some(
        Numeric({ name, shape, dtype, real, imag, is_global: info.is_global, }),
      )
    } else {
      None
    }
  } else if class == 5 {
    let ir = cursor.tag(end, little)
    if ir.kind != 5 ||
      ir.size % 4 != 0 ||
      ir.size / 4 > state.basic.max_elements {
      fail("invalid-sparse", ir.start, "Invalid row vector")
    }
    let slots = ir.size / 4
    charge_elements(state.budget, slots, state.basic)
    if reserved < slots.to_uint64() ||
      reserved > state.basic.max_elements.to_uint64() {
      fail("element-limit", flags.start, "Invalid CSC capacity")
    }
    state.result(element_size(ir.size), keep)
    let (rows, bad) = source_indices(
      cursor,
      ir,
      keep,
      shape[0],
      slots,
      false,
      little,
    )
    let jc = cursor.tag(end, little)
    if jc.kind != 5 || jc.size != (shape[1] + 1) * 4 {
      fail("invalid-sparse", jc.start, "Invalid column pointers")
    }
    state.result(element_size(jc.size), keep)
    let (cols, active) = source_indices(
      cursor, jc, keep, 0, slots, true, little,
    )
    if bad >= 0 && bad < active {
      fail("invalid-sparse", ir.start + bad * 4, "Active row outside shape")
    }
    let tag = cursor.tag(end, little)
    if dtype == Float64 && tag.kind != 9 {
      fail(
        "unsupported-sparse-dtype",
        tag.start,
        "Sparse numeric plane must be double",
      )
    }
    state.result(element_size(slots * dtype.width()), keep)
    let real = source_plane(cursor, tag, slots, dtype, little, keep)
    let imag = if complex {
      let tag = cursor.tag(end, little)
      if tag.kind != 9 {
        fail(
          "unsupported-sparse-dtype",
          tag.start,
          "Sparse imaginary plane must be double",
        )
      }
      state.result(element_size(slots * dtype.width()), keep)
      Some(source_plane(cursor, tag, slots, dtype, little, keep))
    } else {
      None
    }
    if keep {
      Some(
        Csc({
          name,
          shape,
          dtype,
          row_indices: rows,
          col_ptrs: cols,
          real,
          imag,
          nzmax: reserved.to_int(),
          is_global: info.is_global,
        }),
      )
    } else {
      None
    }
  } else if class == 4 {
    let tag = cursor.tag(end, little)
    let encoding : CharEncoding = match tag.kind {
      1 => Int8Text
      2 => UInt8Text
      4 => UInt16Text
      16 => Utf8
      17 => Utf16
      18 => Utf32
      _ => {
        fail(
          "unsupported-char-encoding",
          tag.start,
          "Unknown character encoding",
        )
        Utf8
      }
    }
    state.result(element_size(tag.size), keep)
    let raw = source_character(cursor, tag, encoding, count, little, keep)
    if keep {
      Some(
        Character({
          name,
          shape,
          encoding,
          data: raw,
          is_global: info.is_global,
        }),
      )
    } else {
      None
    }
  } else {
    let class_name = if class == 3 {
      let tag = cursor.tag(end, little)
      if tag.kind != 1 || tag.size > state.basic.max_name_bytes {
        fail("invalid-name", tag.start, "Invalid legacy class name")
      }
      state.metadata(tag.size)
      state.result(element_size(tag.size), keep)
      Some(source_text_value(cursor.payload(tag), state.basic, false))
    } else {
      None
    }
    let fields : Array[String] = []
    if class == 2 || class == 3 {
      let tag = cursor.tag(end, little)
      if tag.kind != 5 || tag.size != 4 {
        fail("invalid-fields", tag.start, "Expected field width")
      }
      let data = cursor.payload(tag)
      let width = bits_at(data, 0, 4, little)
      if width < 1UL || width > (state.basic.max_name_bytes + 1).to_uint64() {
        fail("invalid-fields", tag.start, "Invalid field width")
      }
      let width = width.to_int()
      let table = cursor.tag(end, little)
      if table.kind != 1 ||
        table.size % width != 0 ||
        table.size / width > state.tree.max_fields {
        fail("invalid-fields", table.start, "Invalid field table")
      }
      state.metadata(table.size)
      let data = cursor.payload(table)
      let seen : Map[String, Bool] = Map([])
      let mut canonical_width = 1
      for i = 0; i < data.length() / width; i = i + 1 {
        let start = i * width
        let mut length = 0
        while length < width && data[start + length] != b'\x00' {
          length = length + 1
        }
        if length == width {
          fail(
            "invalid-fields",
            table.start + start,
            "Missing field terminator",
          )
        }
        for j = length; j < width; j = j + 1 {
          if data[start + j] != b'\x00' {
            fail(
              "invalid-fields",
              table.start + start + j,
              "Nonzero field padding",
            )
          }
        }
        let field = source_text_value(
          data[start:start + length].to_owned(),
          state.basic,
          false,
        )
        if seen.contains(field) {
          fail("duplicate-field", table.start + start, "Duplicate field")
        }
        seen[field] = true
        fields.push(field)
        if length + 1 > canonical_width {
          canonical_width = length + 1
        }
      }
      state.result(8 + element_size(canonical_width * fields.length()), keep)
    }
    let multiplier = if class == 1 { 1 } else { fields.length() }
    if multiplier > 0 &&
      count > (state.tree.max_nodes - state.budget.nodes) / multiplier {
      fail("node-limit", cursor.position, "Container children exceed quota")
    }
    let values = []
    for _i = 0; _i < count * multiplier; _i = _i + 1 {
      let tag = cursor.tag(end, little)
      let (child, _) = source_stream_node(
        cursor,
        tag,
        little,
        state,
        depth + 1,
        keep,
      )
      if child is Some(n) {
        values.push(n)
      }
    }
    if keep {
      if class == 1 {
        Some(Cell({ name, shape, values, is_global: info.is_global, }))
      } else {
        Some(
          Structure({
            name,
            shape,
            values,
            fields,
            class_name,
            is_global: info.is_global,
          }),
        )
      }
    } else {
      None
    }
  }
  if cursor.position != end {
    fail(
      "trailing-matrix-data",
      cursor.position,
      "Unexpected trailing matrix content",
    )
  }
  cursor.finish(outer)
  (value, info)
}

///|
fn scan_source_stream(
  source : MatSource,
  selection : Array[String]?,
  collect : Bool,
  limits : SourceLimits,
  tree : TreeLimits,
) -> (MatTree, Array[MatRootInfo]) raise MatError {
  let basic = source_limits(limits)
  validate_tree_limits(tree)
  if source.size < 128 {
    fail("invalid-header", 0, "MAT header needs 128 bytes")
  }
  if source.size > limits.max_source_bytes {
    fail("file-limit", 0, "Source exceeds input quota")
  }
  let cursor : SourceCursor = {
    source,
    producer: None,
    buffer: FixedArray::make(limits.chunk_bytes, b'\x00'),
    position: 0,
    start: 0,
    available: 0,
  }
  let header = cursor.owned(128)
  let file = read_tree(header, limits=basic, tree_limits=tree)
  let little = file.little_endian
  let wanted : Map[String, Bool]? = selection.map(names => {
    let map : Map[String, Bool] = Map([])
    for name in names {
      map[name] = true
    }
    map
  })
  let state : SourceScan = {
    limits,
    basic,
    tree,
    wanted,
    result_size: 128,
    expanded: 0,
    budget: {
      nodes: 0,
      elements: 0,
      metadata: 0,
      metadata_limit: limits.max_metadata_bytes,
    },
  }
  let variables = []
  let roots = []
  let seen : Map[String, Bool] = Map([])
  let kept : Map[String, MatNode] = Map([])
  let mut count = 0
  while cursor.position < source.size {
    if count >= limits.max_variables {
      fail("variable-limit", cursor.position, "Too many roots")
    }
    let outer = cursor.tag(source.size, little)
    let (node, root) = if outer.kind == 14 {
      source_stream_node(cursor, outer, little, state, 1, false)
    } else if outer.kind == 15 {
      let inflater = make_source_inflater(
        cursor,
        outer.start + outer.size,
        limits.max_expanded_bytes - state.expanded,
      )
      let header = FixedArray::make(8, b'\x00')
      for i = 0; i < 8; i = i + 1 {
        match inflater.next() {
          Some(byte) => header[i] = byte
          None =>
            fail("invalid-compressed", outer.start, "Missing matrix header")
        }
      }
      let prefix = Bytes::makei(8, i => header[i])
      let kind = bits_at(prefix, 0, 4, little)
      let body = bits_at(prefix, 4, 4, little)
      let length = 8UL + body + (8UL - body % 8UL) % 8UL
      if kind != 14UL {
        fail(
          "invalid-compressed",
          outer.start,
          "Compressed root must contain a matrix",
        )
      }
      if length > (limits.max_expanded_bytes - state.expanded).to_uint64() {
        fail("expanded-limit", outer.start, "Expanded matrix exceeds quota")
      }
      let buffer = FixedArray::make(
        if limits.chunk_bytes < 8 {
          8
        } else {
          limits.chunk_bytes
        },
        b'\x00',
      )
      for i = 0; i < 8; i = i + 1 {
        buffer[i] = header[i]
      }
      let virtual_source : MatSource = {
        size: length.to_int(),
        read_into: (_a, _b, _c, _d) => -1,
      }
      let expanded : SourceCursor = {
        source: virtual_source,
        producer: Some(inflater),
        buffer,
        position: 0,
        start: 0,
        available: 8,
      }
      let matrix = expanded.tag(virtual_source.size, little)
      let (value, root) = source_stream_node(
        expanded, matrix, little, state, 1, false,
      )
      if expanded.position != virtual_source.size {
        fail("invalid-compressed", outer.start, "Trailing matrix data")
      }
      if inflater.next() is Some(_) {
        fail("invalid-compressed", outer.start, "Extra expanded content")
      }
      state.expanded = state.expanded + inflater.produced
      cursor.finish(outer)
      (value, { ..root, compressed: true, })
    } else {
      fail("unsupported-element", outer.start, "Unsupported root element")
      abort("unreachable")
    }
    if seen.contains(root.name) {
      fail("duplicate-name", outer.start, "Duplicate root")
    }
    seen[root.name] = true
    if node is Some(value) {
      variables.push(value)
      kept[root.name] = value
    }
    if collect {
      roots.push(root)
    }
    count = count + 1
  }
  let ordered = if selection is Some(names) {
    let out = []
    let requested : Map[String, Bool] = Map([])
    for name in names {
      if requested.contains(name) {
        fail("duplicate-name", 0, "Repeated selection")
      }
      requested[name] = true
      match kept.get(name) {
        Some(n) => out.push(n)
        None => fail("missing-variable", 0, "Missing variable: " + name)
      }
    }
    out
  } else {
    variables
  }
  ({ ..file, variables: ordered, }, roots)
}