///|
/// A materialized value. Catalog components become elements; builtins stay AST.
priv enum Val {
  Null
  Boolean(Bool)
  Num(Double, String)
  Str(String)
  Arr(Array[Val])
  Obj(Array[(String, Val)])
  Element(String, String, Array[(String, Val)], Bool, Bool)
  Runtime(Expr)
}

///|
priv struct Issue {
  code : String
  component : String
  path : String
  message : String
  line : Int32
  column : Int32
}

///|
priv struct Mat {
  program : Program
  catalog : SchemaCatalog
  defs : Map[String, Int32]
  unresolved : Array[String]
  mut reached : Array[String]
  visited : Map[String, Bool]
  mut visiting : Array[String]
  errors : Array[Issue]
  use_cache : Bool
  cache : Ref[Map[String, Val]]
  cache_limit : Int32
}

///|
let cached_schema : Ref[String] = Ref("")

///|
/// JSON for stream leaves. Empty while a one-shot parse is encoding.
let encoded_elements : Ref[Map[String, String]] = Ref(Map([]))

///|
let cached_catalog : Ref[SchemaCatalog] = Ref({
  components: [],
  calls: [],
  errors: [],
})

///|
fn catalog_for(schema : String) -> SchemaCatalog {
  if schema == cached_schema.val {
    cached_catalog.val
  } else {
    let catalog = compile_schema(schema)
    cached_schema.val = schema
    cached_catalog.val = catalog
    catalog
  }
}

///|
fn clone_expr(expr : Expr) -> Expr {
  match expr {
    Null => Null
    Bool(value) => Bool(value)
    Num(value, repr) => Num(value, repr)
    Str(text) => Str(text)
    Ref(name) => Ref(name)
    StateRef(name) => StateRef(name)
    Comp(name, is_function, args) => {
      let copied : Array[Expr] = []
      for arg in args {
        copied.push(clone_expr(arg))
      }
      Comp(name, is_function, copied)
    }
    Arr(items) => {
      let copied : Array[Expr] = []
      for item in items {
        copied.push(clone_expr(item))
      }
      Arr(copied)
    }
    Obj(entries) => {
      let copied : Array[(String, Expr)] = []
      for entry in entries {
        let (key, value) = entry
        copied.push((key, clone_expr(value)))
      }
      Obj(copied)
    }
    UnaryOp(op, operand) => UnaryOp(op, clone_expr(operand))
    BinOp(op, left, right) => BinOp(op, clone_expr(left), clone_expr(right))
    Ternary(cond, then_branch, else_branch) =>
      Ternary(
        clone_expr(cond),
        clone_expr(then_branch),
        clone_expr(else_branch),
      )
    Member(obj, field) => Member(clone_expr(obj), field)
    Index(obj, index) => Index(clone_expr(obj), clone_expr(index))
    Assign(target, value) => Assign(target, clone_expr(value))
  }
}

///|
fn starts_with_dollar(name : String) -> Bool {
  name.length() > 0 && name.at(0).to_int() == code('$')
}

///|
fn list_has(items : Array[String], name : String) -> Bool {
  for item in items {
    if item == name {
      return true
    }
  }
  false
}

///|
fn push_unique(items : Array[String], name : String) -> Unit {
  if !list_has(items, name) {
    items.push(name)
  }
}

///|
/// Index of the outermost construct that is still unclosed, or -1.
/// The scan itself is `lexscan` over the source.
fn unclosed_at(src : String) -> Int32 {
  hold_from(src)
}

///|
/// Unclosed strings or brackets mean the trailing construct is still incomplete.
pub fn source_incomplete(src : String) -> Bool {
  unclosed_at(src) >= 0
}

///|
fn index_statements(program : Program) -> Map[String, Int32] {
  let defs : Map[String, Int32] = Map([], capacity=program.statements.length())
  let mut index = 0
  for stmt in program.statements {
    defs[stmt.id] = index
    index += 1
  }
  defs
}

///|
fn last_statement(
  program : Program,
  defs : Map[String, Int32],
  id : String,
) -> Statement? {
  match defs.get(id) {
    Some(index) => Some(program.statements[index])
    None => None
  }
}

///|
fn is_runtime_name(catalog : SchemaCatalog, name : String) -> Bool {
  if is_builtin(name) || name == "Query" || name == "Mutation" {
    return true
  }
  for call in catalog.calls {
    if call.name == name {
      return true
    }
  }
  false
}

///|
fn component_params(catalog : SchemaCatalog, name : String) -> Array[ParamDef]? {
  for component in catalog.components {
    if component.name == name {
      return Some(component.params)
    }
  }
  None
}

///|
fn push_issue(
  mat : Mat,
  code : String,
  component : String,
  path : String,
  message : String,
  line : Int32,
) -> Unit {
  mat.errors.push({ code, component, path, message, line, column: 0, })
}

///|
fn literal_display(value : Val) -> String? {
  match value {
    Null => Some("null")
    Boolean(true) => Some("true")
    Boolean(false) => Some("false")
    Num(_, repr) => Some(repr)
    Str(text) => Some(json_quote(text))
    _ => None
  }
}

///|
fn shape_label(shape : SchemaShape) -> String {
  match shape {
    Union(items) => {
      let labels : Array[String] = []
      for item in items {
        labels.push(item.label())
      }
      join_labels(labels, " | ")
    }
    _ => shape.label()
  }
}

///|
fn matches_shape(shape : SchemaShape, value : Val) -> Bool {
  match value {
    Runtime(_) => true
    _ =>
      match shape {
        Any | Ref(_) | Unknown(_) => true
        Str =>
          match value {
            Str(_) => true
            _ => false
          }
        Num =>
          match value {
            Num(_) => true
            _ => false
          }
        Boolean =>
          match value {
            Boolean(_) => true
            _ => false
          }
        Obj =>
          match value {
            Obj(_) => true
            _ => false
          }
        Arr =>
          match value {
            Arr(_) => true
            _ => false
          }
        Null =>
          match value {
            Null => true
            _ => false
          }
        Enum(items) =>
          match literal_display(value) {
            Some(text) => list_has(items, text)
            None => false
          }
        Union(items) => {
          for item in items {
            if matches_shape(item, value) {
              return true
            }
          }
          false
        }
      }
  }
}

///|
fn materialize_expr(mat : Mat, expr : Expr, line : Int32) -> Val {
  match expr {
    Null => Null
    Bool(value) => Boolean(value)
    Num(value, repr) => Num(value, repr)
    Str(text) => Str(text)
    StateRef(_) as expr => Runtime(expr)
    Ref(name) => materialize_ref(mat, name, line)
    Arr(items) => {
      let values : Array[Val] = []
      for item in items {
        let value = materialize_expr(mat, item, line)
        match (item, value) {
          (Comp(_, _, _), Null) | (Ref(_), Null) => ()
          _ => values.push(value)
        }
      }
      Arr(values)
    }
    Obj(entries) => {
      let fields : Array[(String, Val)] = []
      for entry in entries {
        let (key, value) = entry
        fields.push((key, materialize_expr(mat, value, line)))
      }
      Obj(fields)
    }
    Comp(name, is_function, args) =>
      materialize_call(mat, name, is_function, args, "", line)
    other => Runtime(other)
  }
}

///|
fn expr_has_ref(expr : Expr) -> Bool {
  match expr {
    Ref(_) => true
    Arr(items) => {
      for item in items {
        if expr_has_ref(item) {
          return true
        }
      }
      false
    }
    Obj(entries) => {
      for entry in entries {
        let (_, value) = entry
        if expr_has_ref(value) {
          return true
        }
      }
      false
    }
    Comp(_, _, args) => {
      for arg in args {
        if expr_has_ref(arg) {
          return true
        }
      }
      false
    }
    UnaryOp(_, value) => expr_has_ref(value)
    BinOp(_, left, right) => expr_has_ref(left) || expr_has_ref(right)
    Ternary(cond, yes, no) =>
      expr_has_ref(cond) || expr_has_ref(yes) || expr_has_ref(no)
    Member(value, _) => expr_has_ref(value)
    Index(value, index) => expr_has_ref(value) || expr_has_ref(index)
    Assign(_, value) => expr_has_ref(value)
    _ => false
  }
}

///|
fn materialize_ref(mat : Mat, name : String, line : Int32) -> Val {
  if mat.use_cache {
    match mat.cache.val.get(name) {
      Some(value) => {
        mat.visited[name] = true
        return value
      }
      None => ()
    }
  }
  if list_has(mat.visiting, name) {
    push_unique(mat.unresolved, name)
    return Null
  }
  match last_statement(mat.program, mat.defs, name) {
    None => {
      push_unique(mat.unresolved, name)
      Null
    }
    Some(stmt) => {
      mat.visited[name] = true
      mat.visiting.push(name)
      let value = match stmt.expr {
        Comp(comp_name, is_function, args) =>
          materialize_call(mat, comp_name, is_function, args, name, stmt.line)
        other => materialize_expr(mat, other, stmt.line)
      }
      let next : Array[String] = []
      for item in mat.visiting {
        if item != name {
          next.push(item)
        }
      }
      mat.visiting = next
      if mat.use_cache &&
        mat.defs[name] < mat.cache_limit &&
        !expr_has_ref(stmt.expr) {
        mat.cache.val[name] = value
        let buf = StringBuilder()
        write_val(buf, value, false)
        encoded_elements.val[name] = buf.to_string()
      }
      let _line = line
      value
    }
  }
}

///|
fn materialize_call(
  mat : Mat,
  name : String,
  is_function : Bool,
  args : Array[Expr],
  statement_id : String,
  line : Int32,
) -> Val {
  if is_function && !is_runtime_name(mat.catalog, name) {
    push_issue(
      mat,
      "unknown-function",
      name,
      "",
      "Unknown function \"@" + name + "\"",
      line,
    )
    return Null
  }
  if name == "Query" || name == "Mutation" {
    push_issue(
      mat,
      "inline-reserved",
      name,
      "",
      name +
      "() must be declared as a top-level statement, not used inline as a value",
      line,
    )
    return Null
  }
  if is_runtime_name(mat.catalog, name) {
    let copied : Array[Expr] = []
    for arg in args {
      copied.push(clone_expr(arg))
    }
    return Runtime(Comp(name, is_function, copied))
  }
  match component_params(mat.catalog, name) {
    None => {
      push_issue(
        mat,
        "unknown-component",
        name,
        "",
        "Unknown component \"" + name + "\"",
        line,
      )
      Null
    }
    Some(params) => {
      let props : Array[(String, Val)] = []
      let mismatched : Array[String] = []
      let mut index = 0
      let mut drop_component = false
      while index < params.length() && index < args.length() {
        let param = params[index]
        let value = materialize_expr(mat, args[index], line)
        if matches_shape(param.shape, value) {
          props.push((param.name, value))
        } else {
          let expected = shape_label(param.shape)
          let actual = match literal_display(value) {
            Some(text) => text
            None => "value"
          }
          push_issue(
            mat,
            "type-mismatch",
            name,
            "/" + param.name,
            "field \"/" +
            param.name +
            "\" expects " +
            expected +
            " but got " +
            actual,
            line,
          )
          if param.required {
            drop_component = true
            mismatched.push(param.name)
          }
        }
        index += 1
      }
      if args.length() > params.length() {
        let extra = args.length() - params.length()
        push_issue(
          mat,
          "excess-args",
          name,
          "",
          name +
          " takes " +
          params.length().to_string() +
          " arg(s), got " +
          args.length().to_string() +
          " (" +
          extra.to_string() +
          " excess dropped)",
          line,
        )
      }
      for param in params {
        if param.required && !list_has(mismatched, param.name) {
          let mut present = false
          for prop in props {
            let (key, value) = prop
            if key == param.name {
              match value {
                Null => ()
                _ => present = true
              }
            }
          }
          if !present {
            drop_component = true
            push_issue(
              mat,
              "missing-required",
              name,
              "/" + param.name,
              "missing required field \"/" + param.name + "\"",
              line,
            )
          }
        }
      }
      if drop_component {
        Null
      } else {
        Element(name, statement_id, props, false, props_have_dynamic(props))
      }
    }
  }
}

///|
fn pick_root(
  program : Program,
  defs : Map[String, Int32],
  root_name : String,
) -> (String, Bool)? {
  match last_statement(program, defs, "root") {
    Some(_) => Some(("root", false))
    None =>
      if root_name == "" {
        None
      } else {
        let mut found : String? = None
        for stmt in program.statements {
          match stmt.expr {
            Comp(name, false, _) =>
              if name == root_name &&
                !is_builtin(name) &&
                name != "Query" &&
                name != "Mutation" {
                if found is None {
                  found = Some(stmt.id)
                }
              }
            _ => ()
          }
        }
        match found {
          Some(id) => Some((id, true))
          None => None
        }
      }
  }
}

///|
fn orphan_ids(
  program : Program,
  root_id : String,
  visited : Map[String, Bool],
) -> Array[String] {
  let ids : Array[String] = []
  let seen : Map[String, Bool] = Map([])
  for stmt in program.statements {
    if stmt.id != root_id &&
      !starts_with_dollar(stmt.id) &&
      !visited.contains(stmt.id) &&
      !seen.contains(stmt.id) {
      seen[stmt.id] = true
      ids.push(stmt.id)
    }
  }
  ids
}

///|
fn Mat::run(self : Mat, root_name : String, incomplete : Bool) -> Val? {
  for err in self.program.errors {
    self.errors.push({
      code: "parse-failed",
      component: "",
      path: "",
      message: err.message(),
      line: err.line,
      column: err.column,
    })
  }
  for err in self.catalog.errors {
    self.errors.push({
      code: "schema",
      component: "",
      path: err.path,
      message: err.message(),
      line: 0,
      column: 0,
    })
  }
  match pick_root(self.program, self.defs, root_name) {
    None => {
      let component = if root_name == "" { "" } else { root_name }
      push_issue(self, "no-root", component, "", "No root statement", 0)
      None
    }
    Some((id, fallback)) => {
      let root = match last_statement(self.program, self.defs, id) {
        None => None
        Some(stmt) =>
          match stmt.expr {
            Comp(name, is_function, args) =>
              match
                materialize_call(self, name, is_function, args, id, stmt.line) {
                Element(type_name, _, props, _, dynamic) =>
                  Some(Element(type_name, id, props, incomplete, dynamic))
                Null => None
                other => Some(other)
              }
            other =>
              match materialize_expr(self, other, stmt.line) {
                Element(type_name, _, props, _, dynamic) =>
                  Some(Element(type_name, id, props, incomplete, dynamic))
                Null => None
                value => Some(value)
              }
          }
      }
      match root {
        Some(Element(_)) =>
          if fallback {
            push_issue(
              self,
              "no-root",
              root_name,
              "",
              "No root statement. Rendering the first statement that calls the root component " +
              root_name,
              0,
            )
          }
        Some(_) =>
          push_issue(
            self, "no-root", root_name, "", "root does not evaluate to a component",
            0,
          )
        None => ()
      }
      self.reached = orphan_ids(self.program, id, self.visited)
      match root {
        Some(Element(type_name, statement_id, props, partial, dynamic)) =>
          Some(Element(type_name, statement_id, props, partial, dynamic))
        _ => None
      }
    }
  }
}

///|
fn write_indent(buf : StringBuilder, level : Int32) -> Unit {
  let mut index = 0
  while index < level {
    buf.write_string("  ")
    index += 1
  }
}

///|
fn Val::is_scalar(self : Val) -> Bool {
  match self {
    Null | Boolean(_) | Num(_) | Str(_) | Runtime(_) => true
    _ => false
  }
}

///|
fn Val::write(
  self : Val,
  buf : StringBuilder,
  level : Int32,
  bullet : Bool,
) -> Unit {
  write_indent(buf, level)
  if bullet {
    buf.write_string("- ")
  }
  match self {
    Null => buf.write_string("null")
    Boolean(true) => buf.write_string("true")
    Boolean(false) => buf.write_string("false")
    Num(_, repr) => buf.write_string(repr)
    Str(text) => buf.write_string(json_quote(text))
    Runtime(expr) => buf.write_string(expr.dump())
    Element(type_name, _, props, _, _) => {
      buf.write_string(type_name)
      for prop in props {
        let (key, value) = prop
        buf.write_char('\n')
        if value.is_scalar() {
          write_indent(buf, level + 1)
          buf.write_string(key + ": ")
          match value {
            Null => buf.write_string("null")
            Boolean(true) => buf.write_string("true")
            Boolean(false) => buf.write_string("false")
            Num(_, repr) => buf.write_string(repr)
            Str(text) => buf.write_string(json_quote(text))
            Runtime(expr) => buf.write_string(expr.dump())
            _ => ()
          }
        } else {
          write_indent(buf, level + 1)
          buf.write_string(key + ":")
          buf.write_char('\n')
          match value {
            Arr(items) => {
              let mut item_index = 0
              for item in items {
                if item_index > 0 {
                  buf.write_char('\n')
                }
                item.write(buf, level + 2, true)
                item_index += 1
              }
            }
            Obj(fields) => {
              let mut field_index = 0
              for field in fields {
                if field_index > 0 {
                  buf.write_char('\n')
                }
                let (field_key, field_value) = field
                write_indent(buf, level + 2)
                buf.write_string(field_key + ": ")
                match field_value {
                  Str(text) => buf.write_string(json_quote(text))
                  Boolean(true) => buf.write_string("true")
                  Boolean(false) => buf.write_string("false")
                  Num(_, repr) => buf.write_string(repr)
                  Null => buf.write_string("null")
                  other => {
                    buf.write_char('\n')
                    other.write(buf, level + 3, false)
                  }
                }
                field_index += 1
              }
            }
            other => other.write(buf, level + 2, false)
          }
        }
      }
    }
    Arr(items) =>
      for item in items {
        buf.write_char('\n')
        item.write(buf, level, true)
      }
    Obj(_) => buf.write_string("{...}")
  }
}

///|
fn issue_dump(errors : Array[Issue]) -> String {
  let buf = StringBuilder()
  for err in errors {
    buf.write_char('\n')
    buf.write_string(err.line.to_string())
    buf.write_char(':')
    buf.write_string(err.column.to_string())
    buf.write_char(' ')
    buf.write_string(err.code)
    buf.write_char(' ')
    buf.write_string(err.message)
  }
  buf.to_string()
}

///|
fn names_dump(items : Array[String]) -> String {
  if items.length() == 0 {
    ""
  } else {
    " " + join_labels(items, " ")
  }
}

///|
fn result_dump(
  root : Val?,
  incomplete : Bool,
  unresolved : Array[String],
  orphaned : Array[String],
  errors : Array[Issue],
) -> String {
  let buf = StringBuilder()
  buf.write_string("incomplete: ")
  buf.write_string(if incomplete { "true" } else { "false" })
  buf.write_string("\nunresolved:" + names_dump(unresolved))
  buf.write_string("\norphaned:" + names_dump(orphaned))
  buf.write_string("\nerrors: " + errors.length().to_string())
  buf.write_string(issue_dump(errors))
  match root {
    Some(value) => {
      buf.write_char('\n')
      value.write(buf, 0, false)
    }
    None => buf.write_string("\nroot: null")
  }
  buf.to_string()
}

///|
fn val_json(value : Val) -> Json {
  match value {
    Null => Json::null()
    Boolean(true) => Json::boolean(true)
    Boolean(false) => Json::boolean(false)
    Num(number, repr) => Json::number(number, repr~)
    Str(text) => Json::string(text)
    Runtime(expr) => expr.to_json()
    Arr(items) => {
      let encoded : Array[Json] = []
      for item in items {
        encoded.push(val_json(item))
      }
      Json::array(encoded)
    }
    Obj(fields) => {
      let encoded : Array[(String, Json)] = []
      for field in fields {
        let (key, item) = field
        encoded.push((key, val_json(item)))
      }
      json_object(encoded)
    }
    Element(type_name, statement_id, props, partial, dynamic) => {
      let prop_fields : Array[(String, Json)] = []
      for prop in props {
        let (key, item) = prop
        prop_fields.push((key, val_json(item)))
      }
      let fields : Array[(String, Json)] = [
        ("type", Json::string("element")),
        ("typeName", Json::string(type_name)),
        ("props", json_object(prop_fields)),
        ("partial", Json::boolean(partial)),
        ("hasDynamicProps", Json::boolean(dynamic)),
      ]
      if statement_id != "" {
        fields.push(("statementId", Json::string(statement_id)))
      }
      json_object(fields)
    }
  }
}

///|
fn json_object(fields : Array[(String, Json)]) -> Json {
  let map : Map[String, Json] = Map([])
  for field in fields {
    let (key, value) = field
    map[key] = value
  }
  Json::object(map)
}

///|
fn result_json(
  root : Val?,
  incomplete : Bool,
  unresolved : Array[String],
  orphaned : Array[String],
  errors : Array[Issue],
) -> String {
  let unresolved_json : Array[Json] = []
  for name in unresolved {
    unresolved_json.push(Json::string(name))
  }
  let orphaned_json : Array[Json] = []
  for name in orphaned {
    orphaned_json.push(Json::string(name))
  }
  let error_json : Array[Json] = []
  for err in errors {
    error_json.push(
      json_object([
        ("code", Json::string(err.code)),
        ("component", Json::string(err.component)),
        ("path", Json::string(err.path)),
        ("message", Json::string(err.message)),
        ("line", Json::number(Double::from_int(err.line))),
        ("column", Json::number(Double::from_int(err.column))),
      ]),
    )
  }
  let root_json = match root {
    Some(value) => val_json(value)
    None => Json::null()
  }
  json_object([
    ("root", root_json),
    ("incomplete", Json::boolean(incomplete)),
    ("unresolved", Json::array(unresolved_json)),
    ("orphaned", Json::array(orphaned_json)),
    ("errors", Json::array(error_json)),
  ]).stringify(indent=2)
}

///|
fn materialize_with(
  src : String,
  schema : String,
  root_name : String,
) -> (Val?, Bool, Array[String], Array[String], Array[Issue], Int32) {
  let program = parse(src)
  let count = program.statements.length()
  let catalog = catalog_for(schema)
  let defs = index_statements(program)
  let incomplete = source_incomplete(src)
  let mat = Mat::{
    program,
    catalog,
    defs,
    unresolved: [],
    reached: [],
    visited: Map([]),
    visiting: [],
    errors: [],
    use_cache: false,
    cache: Ref(Map([])),
    cache_limit: 0,
  }
  let root = mat.run(root_name, incomplete)
  (root, incomplete, mat.unresolved, mat.reached, mat.errors, count)
}

///|
fn materialize_program(
  program : Program,
  schema : String,
  root_name : String,
  incomplete : Bool,
  cache : Ref[Map[String, Val]],
  cache_limit : Int32,
) -> (Val?, Bool, Array[String], Array[String], Array[Issue], Int32) {
  let count = program.statements.length()
  let catalog = catalog_for(schema)
  let defs = index_statements(program)
  let mat = Mat::{
    program,
    catalog,
    defs,
    unresolved: [],
    reached: [],
    visited: Map([]),
    visiting: [],
    errors: [],
    use_cache: true,
    cache,
    cache_limit,
  }
  let root = mat.run(root_name, incomplete)
  (root, incomplete, mat.unresolved, mat.reached, mat.errors, count)
}

///|
/// Materialize one statement. Callers use this for a closed leaf during streaming.
fn materialize_statement(
  stmt : Statement,
  schema : String,
) -> (Val, Array[Issue]) {
  let program : Program = { statements: [stmt], errors: [], }
  let catalog = catalog_for(schema)
  let defs : Map[String, Int32] = Map([])
  defs[stmt.id] = 0
  let saved = encoded_elements.val
  encoded_elements.val = Map([])
  let mat = Mat::{
    program,
    catalog,
    defs,
    unresolved: [],
    reached: [],
    visited: Map([]),
    visiting: [],
    errors: [],
    use_cache: false,
    cache: Ref(Map([])),
    cache_limit: 0,
  }
  let value = match stmt.expr {
    Comp(name, is_function, args) =>
      materialize_call(mat, name, is_function, args, stmt.id, stmt.line)
    other => materialize_expr(mat, other, stmt.line)
  }
  encoded_elements.val = saved
  (value, mat.errors)
}

///|
fn scalar_json(expr : Expr) -> String? {
  match expr {
    Null => Some("null")
    Bool(true) => Some("true")
    Bool(false) => Some("false")
    Num(_, repr) => Some(repr)
    Str(text) => {
      let buf = StringBuilder()
      write_json_string(buf, text)
      Some(buf.to_string())
    }
    _ => None
  }
}

///|
fn scalar_display(expr : Expr) -> String? {
  match expr {
    Null => Some("null")
    Bool(true) => Some("true")
    Bool(false) => Some("false")
    Num(_, repr) => Some(repr)
    Str(text) => Some(json_quote(text))
    _ => None
  }
}

///|
fn scalar_matches(shape : SchemaShape, expr : Expr) -> Bool {
  match shape {
    Any | Ref(_) | Unknown(_) => true
    Str =>
      match expr {
        Str(_) => true
        _ => false
      }
    Num =>
      match expr {
        Num(_) => true
        _ => false
      }
    Boolean =>
      match expr {
        Bool(_) => true
        _ => false
      }
    Null =>
      match expr {
        Null => true
        _ => false
      }
    Obj | Arr | Enum(_) => false
    Union(items) => {
      for item in items {
        if scalar_matches(item, expr) {
          return true
        }
      }
      false
    }
  }
}

///|
/// Literal component calls skip the value tree. Anything else uses `materialize_statement`.
fn literal_component(
  catalog : SchemaCatalog,
  name : String,
  args : Array[Expr],
  line : Int32,
  statement_id : String,
) -> (String, Array[Issue])? {
  if is_builtin(name) || name == "Query" || name == "Mutation" {
    return None
  }
  let mut index = 0
  while index < args.length() {
    if scalar_json(args[index]) is None {
      return None
    }
    index = index + 1
  }
  let issues : Array[Issue] = []
  match component_params(catalog, name) {
    None => {
      issues.push({
        code: "unknown-component",
        component: name,
        path: "",
        message: "Unknown component \"" + name + "\"",
        line,
        column: 0,
      })
      Some(("", issues))
    }
    Some(params) => {
      let buf = StringBuilder()
      buf.write_string("{\"type\":\"element\",\"typeName\":")
      write_json_string(buf, name)
      buf.write_string(",\"props\":{")
      let mismatched : Array[String] = []
      let mut drop_component = false
      let mut wrote = false
      let mut arg_index = 0
      while arg_index < params.length() && arg_index < args.length() {
        let param = params[arg_index]
        let arg = args[arg_index]
        if scalar_matches(param.shape, arg) {
          if wrote {
            buf.write_char(',')
          }
          wrote = true
          write_json_string(buf, param.name)
          buf.write_char(':')
          match scalar_json(arg) {
            Some(json) => buf.write_string(json)
            None => ()
          }
        } else {
          let actual = match scalar_display(arg) {
            Some(text) => text
            None => "value"
          }
          issues.push({
            code: "type-mismatch",
            component: name,
            path: "/" + param.name,
            message: "field \"/" +
            param.name +
            "\" expects " +
            shape_label(param.shape) +
            " but got " +
            actual,
            line,
            column: 0,
          })
          if param.required {
            drop_component = true
            mismatched.push(param.name)
          }
        }
        arg_index = arg_index + 1
      }
      if args.length() > params.length() {
        let extra = args.length() - params.length()
        issues.push({
          code: "excess-args",
          component: name,
          path: "",
          message: name +
          " takes " +
          params.length().to_string() +
          " arg(s), got " +
          args.length().to_string() +
          " (" +
          extra.to_string() +
          " excess dropped)",
          line,
          column: 0,
        })
      }
      for param in params {
        if param.required && !list_has(mismatched, param.name) {
          let mut present = false
          let mut slot = 0
          while slot < params.length() && slot < args.length() {
            if params[slot].name == param.name &&
              scalar_matches(param.shape, args[slot]) {
              match args[slot] {
                Null => ()
                _ => present = true
              }
            }
            slot = slot + 1
          }
          if !present {
            drop_component = true
            issues.push({
              code: "missing-required",
              component: name,
              path: "/" + param.name,
              message: "missing required field \"/" + param.name + "\"",
              line,
              column: 0,
            })
          }
        }
      }
      buf.write_string("},\"partial\":false,\"hasDynamicProps\":false")
      if statement_id != "" {
        buf.write_string(",\"statementId\":")
        write_json_string(buf, statement_id)
      }
      buf.write_char('}')
      if drop_component {
        Some(("", issues))
      } else {
        Some((buf.to_string(), issues))
      }
    }
  }
}

///|
fn leaf_payload(
  stmt : Statement,
  schema : String,
  catalog : SchemaCatalog,
) -> (String, Array[Issue]) {
  match stmt.expr {
    Comp(name, false, args) =>
      match literal_component(catalog, name, args, stmt.line, stmt.id) {
        Some(payload) => payload
        None => {
          let (value, issues) = materialize_statement(stmt, schema)
          (child_json(value), issues)
        }
      }
    _ => {
      let (value, issues) = materialize_statement(stmt, schema)
      (child_json(value), issues)
    }
  }
}

///|
/// Child JSON for an array slot. `Null` is omitted, matching ref materialize.
fn child_json(value : Val) -> String {
  match value {
    Null => ""
    other => {
      let saved = encoded_elements.val
      encoded_elements.val = Map([])
      let buf = StringBuilder()
      write_val(buf, other, false)
      encoded_elements.val = saved
      buf.to_string()
    }
  }
}

///|
/// Materialize a program against a library JSON Schema.
/// `root_name` is used only when the program has no `root` statement.
pub fn materialize_dump(
  src : String,
  schema : String,
  root_name : String,
) -> String {
  let (root, incomplete, unresolved, orphaned, errors, _count) = materialize_with(
    src, schema, root_name,
  )
  result_dump(root, incomplete, unresolved, orphaned, errors)
}

///|
/// JSON result: element tree, errors, unresolved, orphaned, and incomplete.
pub fn materialize_json(
  src : String,
  schema : String,
  root_name : String,
) -> String {
  let (root, incomplete, unresolved, orphaned, errors, _count) = materialize_with(
    src, schema, root_name,
  )
  result_json(root, incomplete, unresolved, orphaned, errors)
}

///|
fn val_has_dynamic(value : Val) -> Bool {
  match value {
    Null | Boolean(_) | Num(_) | Str(_) => false
    Runtime(_) => true
    Arr(items) => {
      for item in items {
        if val_has_dynamic(item) {
          return true
        }
      }
      false
    }
    Obj(fields) => {
      for field in fields {
        let (key, item) = field
        if key == "steps" {
          match item {
            Arr(_) => return true
            _ => ()
          }
        }
        if val_has_dynamic(item) {
          return true
        }
      }
      false
    }
    Element(_, _, _, _, dynamic) => dynamic
  }
}

///|
fn props_have_dynamic(props : Array[(String, Val)]) -> Bool {
  for prop in props {
    let (_, value) = prop
    match value {
      Null | Boolean(_) | Num(_) | Str(_) => ()
      _ => if val_has_dynamic(value) { return true }
    }
  }
  false
}

///|
fn write_json_bool(buf : StringBuilder, value : Bool) -> Unit {
  if value {
    buf.write_string("true")
  } else {
    buf.write_string("false")
  }
}

///|
fn write_val_fields(buf : StringBuilder, fields : Array[(String, Val)]) -> Unit {
  buf.write_char('{')
  let mut index = 0
  for field in fields {
    if index > 0 {
      buf.write_char(',')
    }
    let (key, value) = field
    write_json_string(buf, key)
    buf.write_char(':')
    write_val(buf, value, false)
    index += 1
  }
  buf.write_char('}')
}

///|
fn write_val(buf : StringBuilder, value : Val, with_id : Bool) -> Unit {
  match value {
    Null => buf.write_string("null")
    Boolean(true) => buf.write_string("true")
    Boolean(false) => buf.write_string("false")
    Num(_, repr) => buf.write_string(repr)
    Str(text) => write_json_string(buf, text)
    Runtime(expr) => write_expr_json(buf, expr)
    Arr(items) => {
      buf.write_char('[')
      let mut index = 0
      for item in items {
        if index > 0 {
          buf.write_char(',')
        }
        write_val(buf, item, false)
        index += 1
      }
      buf.write_char(']')
    }
    Obj(fields) => write_val_fields(buf, fields)
    Element(type_name, statement_id, props, partial, dynamic) => {
      if !with_id && statement_id != "" && encoded_elements.val.length() > 0 {
        match encoded_elements.val.get(statement_id) {
          Some(json) => {
            buf.write_string(json)
            return
          }
          None => ()
        }
      }
      buf.write_string("{\"type\":\"element\",\"typeName\":")
      write_json_string(buf, type_name)
      buf.write_string(",\"props\":")
      write_val_fields(buf, props)
      buf.write_string(",\"partial\":")
      write_json_bool(buf, partial)
      buf.write_string(",\"hasDynamicProps\":")
      write_json_bool(buf, dynamic)
      if statement_id != "" {
        buf.write_string(",\"statementId\":")
        write_json_string(buf, statement_id)
      }
      buf.write_char('}')
    }
  }
}

///|
fn write_string_list(buf : StringBuilder, items : Array[String]) -> Unit {
  buf.write_char('[')
  let mut index = 0
  for item in items {
    if index > 0 {
      buf.write_char(',')
    }
    write_json_string(buf, item)
    index += 1
  }
  buf.write_char(']')
}

///|
fn write_issues(buf : StringBuilder, errors : Array[Issue]) -> Unit {
  buf.write_char('[')
  let mut index = 0
  for err in errors {
    if index > 0 {
      buf.write_char(',')
    }
    buf.write_string("{\"code\":")
    write_json_string(buf, err.code)
    buf.write_string(",\"component\":")
    write_json_string(buf, err.component)
    buf.write_string(",\"path\":")
    write_json_string(buf, err.path)
    buf.write_string(",\"message\":")
    write_json_string(buf, err.message)
    buf.write_string(",\"line\":")
    buf.write_string(err.line.to_string())
    buf.write_string(",\"column\":")
    buf.write_string(err.column.to_string())
    buf.write_char('}')
    index += 1
  }
  buf.write_char(']')
}

///|
/// Statement count after materialize, without encoding the tree.
pub fn materialize_count(
  src : String,
  schema : String,
  root_name : String,
) -> Int32 {
  let (_, _, _, _, _, count) = materialize_with(src, schema, root_name)
  count
}

///|
/// Official `ParseResult` shape, written without building a `Json` map.
pub fn materialize_result(
  src : String,
  schema : String,
  root_name : String,
) -> String {
  encoded_elements.val = Map([])
  let (root, incomplete, unresolved, orphaned, errors, count) = materialize_with(
    src, schema, root_name,
  )
  let buf = StringBuilder()
  buf.write_string("{\"root\":")
  match root {
    Some(value) => write_val(buf, value, true)
    None => buf.write_string("null")
  }
  buf.write_string(",\"meta\":{\"incomplete\":")
  write_json_bool(buf, incomplete)
  buf.write_string(",\"unresolved\":")
  write_string_list(buf, unresolved)
  buf.write_string(",\"orphaned\":")
  write_string_list(buf, orphaned)
  buf.write_string(",\"statementCount\":")
  buf.write_string(count.to_string())
  buf.write_string(",\"errors\":")
  write_issues(buf, errors)
  buf.write_string(
    "},\"stateDeclarations\":{},\"queryStatements\":[],\"mutationStatements\":[]}",
  )
  buf.to_string()
}

///|
fn encode_parse_result(
  root : Val?,
  incomplete : Bool,
  unresolved : Array[String],
  orphaned : Array[String],
  errors : Array[Issue],
  count : Int32,
) -> String {
  let buf = StringBuilder()
  buf.write_string("{\"root\":")
  match root {
    Some(value) => write_val(buf, value, true)
    None => buf.write_string("null")
  }
  buf.write_string(",\"meta\":{\"incomplete\":")
  write_json_bool(buf, incomplete)
  buf.write_string(",\"unresolved\":")
  write_string_list(buf, unresolved)
  buf.write_string(",\"orphaned\":")
  write_string_list(buf, orphaned)
  buf.write_string(",\"statementCount\":")
  buf.write_string(count.to_string())
  buf.write_string(",\"errors\":")
  write_issues(buf, errors)
  buf.write_string(
    "},\"stateDeclarations\":{},\"queryStatements\":[],\"mutationStatements\":[]}",
  )
  buf.to_string()
}