src/Pure/PIDE/command.scala
author wenzelm
Sat, 17 Dec 2022 11:33:13 +0100
changeset 76663 b7546c25e4f0
parent 76473 b45db8030794
child 76680 e95b9c9e17ff
permissions -rw-r--r--
clarified signature;

/*  Title:      Pure/PIDE/command.scala
    Author:     Fabian Immler, TU Munich
    Author:     Makarius

Prover commands with accumulated results from execution.
*/

package isabelle


import scala.collection.immutable.SortedMap


object Command {
  /* blobs */

  object Blob {
    def read_file(name: Document.Node.Name, src_path: Path): Blob = {
      val bytes = Bytes.read(name.path)
      val chunk = Symbol.Text_Chunk(bytes.text)
      Blob(name, src_path, Some((bytes.sha1_digest, chunk)))
    }
  }

  sealed case class Blob(
    name: Document.Node.Name,
    src_path: Path,
    content: Option[(SHA1.Digest, Symbol.Text_Chunk)]
  ) {
    def read_file: Bytes = Bytes.read(name.path)

    def chunk_file: Symbol.Text_Chunk.File =
      Symbol.Text_Chunk.File(name.node)
  }

  object Blobs_Info {
    val none: Blobs_Info = Blobs_Info(Nil)

    def errors(msgs: List[String]): Blobs_Info =
      Blobs_Info(msgs.map(msg => Exn.Exn[Blob](ERROR(msg))))
  }

  sealed case class Blobs_Info(blobs: List[Exn.Result[Blob]], index: Int = -1)



  /** accumulated results from prover **/

  /* results */

  object Results {
    type Entry = (Long, XML.Elem)
    val empty: Results = new Results(SortedMap.empty)
    def make(args: IterableOnce[Results.Entry]): Results =
      args.iterator.foldLeft(empty)(_ + _)
    def merge(args: IterableOnce[Results]): Results =
      args.iterator.foldLeft(empty)(_ ++ _)
  }

  final class Results private(private val rep: SortedMap[Long, XML.Elem]) {
    def is_empty: Boolean = rep.isEmpty
    def defined(serial: Long): Boolean = rep.isDefinedAt(serial)
    def get(serial: Long): Option[XML.Elem] = rep.get(serial)
    def iterator: Iterator[Results.Entry] = rep.iterator

    def + (entry: Results.Entry): Results =
      if (defined(entry._1)) this
      else new Results(rep + entry)

    def ++ (other: Results): Results =
      if (this eq other) this
      else if (rep.isEmpty) other
      else other.iterator.foldLeft(this)(_ + _)

    override def hashCode: Int = rep.hashCode
    override def equals(that: Any): Boolean =
      that match {
        case other: Results => rep == other.rep
        case _ => false
      }
    override def toString: String = iterator.mkString("Results(", ", ", ")")
  }


  /* exports */

  object Exports {
    type Entry = (Long, Export.Entry)
    val empty: Exports = new Exports(SortedMap.empty)
    def merge(args: IterableOnce[Exports]): Exports =
      args.iterator.foldLeft(empty)(_ ++ _)
  }

  final class Exports private(private val rep: SortedMap[Long, Export.Entry]) {
    def is_empty: Boolean = rep.isEmpty
    def iterator: Iterator[Exports.Entry] = rep.iterator

    def + (entry: Exports.Entry): Exports =
      if (rep.isDefinedAt(entry._1)) this
      else new Exports(rep + entry)

    def ++ (other: Exports): Exports =
      if (this eq other) this
      else if (rep.isEmpty) other
      else other.iterator.foldLeft(this)(_ + _)

    override def hashCode: Int = rep.hashCode
    override def equals(that: Any): Boolean =
      that match {
        case other: Exports => rep == other.rep
        case _ => false
      }
    override def toString: String = iterator.mkString("Exports(", ", ", ")")
  }


  /* markups */

  object Markup_Index {
    val markup: Markup_Index = Markup_Index(false, Symbol.Text_Chunk.Default)
    def blob(blob: Blob): Markup_Index = Markup_Index(false, blob.chunk_file)
  }

  sealed case class Markup_Index(status: Boolean, chunk_name: Symbol.Text_Chunk.Name)

  object Markups {
    type Entry = (Markup_Index, Markup_Tree)
    val empty: Markups = new Markups(Map.empty)
    def init(markup: Markup_Tree): Markups = new Markups(Map(Markup_Index.markup -> markup))
    def make(args: IterableOnce[Entry]): Markups =
      args.iterator.foldLeft(empty)(_ + _)
    def merge(args: IterableOnce[Markups]): Markups =
      args.iterator.foldLeft(empty)(_ ++ _)
  }

  final class Markups private(private val rep: Map[Markup_Index, Markup_Tree]) {
    def is_empty: Boolean = rep.isEmpty

    def apply(index: Markup_Index): Markup_Tree =
      rep.getOrElse(index, Markup_Tree.empty)

    def add(index: Markup_Index, markup: Text.Markup): Markups =
      new Markups(rep + (index -> (this(index) + markup)))

    def + (entry: Markups.Entry): Markups = {
      val (index, tree) = entry
      new Markups(rep + (index -> (this(index).merge(tree, Text.Range.full, Markup.Elements.full))))
    }

    def ++ (other: Markups): Markups =
      if (this eq other) this
      else if (rep.isEmpty) other
      else other.rep.iterator.foldLeft(this)(_ + _)

    def redirection_iterator: Iterator[Document_ID.Generic] =
      for (Markup_Index(_, Symbol.Text_Chunk.Id(id)) <- rep.keysIterator)
        yield id

    def redirect(other_id: Document_ID.Generic): Markups = {
      val rep1 =
        (for {
          (Markup_Index(status, Symbol.Text_Chunk.Id(id)), markup) <- rep.iterator
          if other_id == id
        } yield (Markup_Index(status, Symbol.Text_Chunk.Default), markup)).toMap
      if (rep1.isEmpty) Markups.empty else new Markups(rep1)
    }

    override def hashCode: Int = rep.hashCode
    override def equals(that: Any): Boolean =
      that match {
        case other: Markups => rep == other.rep
        case _ => false
      }
    override def toString: String = rep.iterator.mkString("Markups(", ", ", ")")
  }


  /* state */

  object State {
    def get_result(states: List[State], serial: Long): Option[XML.Elem] =
      states.find(st => st.results.defined(serial)).map(st => st.results.get(serial).get)

    def get_result_proper(states: List[State], props: Properties.T): Option[Results.Entry] =
      for {
        serial <- Markup.Serial.unapply(props)
        elem <- get_result(states, serial)
        if elem.body.nonEmpty
      } yield serial -> elem

    def merge_results(states: List[State]): Results =
      Results.merge(states.map(_.results))

    def merge_exports(states: List[State]): Exports =
      Exports.merge(states.map(_.exports))

    def merge_markups(states: List[State]): Markups =
      Markups.merge(states.map(_.markups))

    def merge_markup(states: List[State], index: Markup_Index,
        range: Text.Range, elements: Markup.Elements): Markup_Tree =
      Markup_Tree.merge(states.map(_.markup(index)), range, elements)

    def merge(command: Command, states: List[State]): State =
      State(command, states.flatMap(_.status), merge_results(states),
        merge_exports(states), merge_markups(states))
  }

  sealed case class State(
    command: Command,
    status: List[Markup] = Nil,
    results: Results = Results.empty,
    exports: Exports = Exports.empty,
    markups: Markups = Markups.empty
  ) {
    lazy val initialized: Boolean = status.exists(markup => markup.name == Markup.INITIALIZED)
    lazy val consolidating: Boolean = status.exists(markup => markup.name == Markup.CONSOLIDATING)
    lazy val consolidated: Boolean = status.exists(markup => markup.name == Markup.CONSOLIDATED)

    lazy val maybe_consolidated: Boolean = {
      var touched = false
      var forks = 0
      var runs = 0
      for (markup <- status) {
        markup.name match {
          case Markup.FORKED => touched = true; forks += 1
          case Markup.JOINED => forks -= 1
          case Markup.RUNNING => touched = true; runs += 1
          case Markup.FINISHED => runs -= 1
          case _ =>
        }
      }
      touched && forks == 0 && runs == 0
    }

    lazy val document_status: Document_Status.Command_Status = {
      val warnings =
        if (results.iterator.exists(p => Protocol.is_warning(p._2) || Protocol.is_legacy(p._2)))
          List(Markup(Markup.WARNING, Nil))
        else Nil
      val errors =
        if (results.iterator.exists(p => Protocol.is_error(p._2)))
          List(Markup(Markup.ERROR, Nil))
        else Nil
      Document_Status.Command_Status.make((warnings ::: errors ::: status).iterator)
    }

    def markup(index: Markup_Index): Markup_Tree = markups(index)

    def redirect(other_command: Command): Option[State] = {
      val markups1 = markups.redirect(other_command.id)
      if (markups1.is_empty) None
      else Some(new State(other_command, markups = markups1))
    }

    private def add_status(st: Markup): State =
      copy(status = st :: status)

    private def add_result(entry: Results.Entry): State =
      copy(results = results + entry)

    def add_export(entry: Exports.Entry): Option[State] =
      if (command.node_name.theory == entry._2.theory_name) Some(copy(exports = exports + entry))
      else None

    private def add_markup(
      status: Boolean,
      chunk_name: Symbol.Text_Chunk.Name,
      m: Text.Markup
    ): State = {
      val markups1 =
        if (status || Document_Status.Command_Status.liberal_elements(m.info.name))
          markups.add(Markup_Index(true, chunk_name), m)
        else markups
      copy(markups = markups1.add(Markup_Index(false, chunk_name), m))
    }

    def accumulate(
        self_id: Document_ID.Generic => Boolean,
        other_id: (Document.Node.Name, Document_ID.Generic) =>
          Option[(Symbol.Text_Chunk.Id, Symbol.Text_Chunk)],
        message: XML.Elem,
        cache: XML.Cache): State =
      message match {
        case XML.Elem(Markup(Markup.STATUS, _), msgs) =>
          if (command.span.is_theory) this
          else {
            msgs.foldLeft(this) {
              case (state, msg) =>
                msg match {
                  case elem @ XML.Elem(markup, Nil) =>
                    state.
                      add_status(markup).
                      add_markup(true, Symbol.Text_Chunk.Default, Text.Info(command.core_range, elem))
                  case _ =>
                    Output.warning("Ignored status message: " + msg)
                    state
                }
            }
          }

        case XML.Elem(Markup(Markup.REPORT, atts0), msgs) =>
          msgs.foldLeft(this) {
            case (state, msg) =>
              def bad(): Unit = Output.warning("Ignored report message: " + msg)

              msg match {
                case XML.Elem(Markup(name, atts), args) =>
                  command.reported_position(atts) orElse command.reported_position(atts0) match {
                    case Some((id, chunk_name, target_range)) =>
                      val target =
                        if (self_id(id) && command.chunks.isDefinedAt(chunk_name))
                          Some((chunk_name, command.chunks(chunk_name)))
                        else if (chunk_name == Symbol.Text_Chunk.Default)
                          other_id(command.node_name, id)
                        else None

                      (target, target_range) match {
                        case (Some((target_name, target_chunk)), Some(symbol_range))
                        if !symbol_range.is_singularity =>
                          target_chunk.incorporate(symbol_range) match {
                            case Some(range) =>
                              val props = atts.filterNot(Markup.position_property)
                              val elem = cache.elem(XML.Elem(Markup(name, props), args))
                              state.add_markup(false, target_name, Text.Info(range, elem))
                            case None => bad(); state
                          }
                        case _ =>
                          // silently ignore excessive reports
                          state
                      }

                    case _ => bad(); state
                  }
                case _ => bad(); state
              }
          }

        case XML.Elem(Markup(name, props), body) =>
          props match {
            case Markup.Serial(i) =>
              val markup_message =
                cache.elem(Protocol.make_message(body, kind = name, props = props))
              val message_markup =
                cache.elem(XML.elem(Markup(name, props.filter(p => p._1 == Markup.SERIAL))))

              var st = add_result(i -> markup_message)
              if (Protocol.is_inlined(message)) {
                for {
                  (chunk_name, chunk) <- command.chunks.iterator
                  range <- command.message_positions(self_id, chunk_name, chunk, message)
                } st = st.add_markup(false, chunk_name, Text.Info(range, message_markup))
              }
              st

            case _ =>
              Output.warning("Ignored message without serial number: " + message)
              this
          }
      }
  }



  /** static content **/

  /* make commands */

  def apply(
    id: Document_ID.Command,
    node_name: Document.Node.Name,
    blobs_info: Blobs_Info,
    span: Command_Span.Span
  ): Command = {
    val (source, span1) = span.compact_source
    new Command(id, node_name, blobs_info, span1, source, Results.empty, Markups.empty)
  }

  val empty: Command =
    Command(Document_ID.none, Document.Node.Name.empty, Blobs_Info.none, Command_Span.empty)

  def unparsed(
    source: String,
    theory: Boolean = false,
    id: Document_ID.Command = Document_ID.none,
    node_name: Document.Node.Name = Document.Node.Name.empty,
    blobs_info: Blobs_Info = Blobs_Info.none,
    results: Results = Results.empty,
    markups: Markups = Markups.empty
  ): Command = {
    val (source1, span1) = Command_Span.unparsed(source, theory).compact_source
    new Command(id, node_name, blobs_info, span1, source1, results, markups)
  }

  def text(source: String): Command = unparsed(source)

  def rich_text(id: Document_ID.Command, results: Results, body: XML.Body): Command =
    unparsed(XML.content(body), id = id, results = results,
      markups = Markups.init(Markup_Tree.from_XML(body)))


  /* edits and perspective */

  type Edit = (Option[Command], Option[Command])

  object Perspective {
    val empty: Perspective = Perspective(Nil)
  }

  sealed case class Perspective(
    commands: List[Command]  // visible commands in canonical order
  ) {
    def is_empty: Boolean = commands.isEmpty

    def same(that: Perspective): Boolean = {
      val cmds1 = this.commands
      val cmds2 = that.commands
      require(!cmds1.exists(_.is_undefined), "cmds1 not defined")
      require(!cmds2.exists(_.is_undefined), "cmds2 not defined")
      cmds1.length == cmds2.length &&
        (cmds1.iterator zip cmds2.iterator).forall({ case (c1, c2) => c1.id == c2.id })
    }
  }


  /* blobs: inlined errors and auxiliary files */

  def blobs_info(
    resources: Resources,
    syntax: Outer_Syntax,
    get_blob: Document.Node.Name => Option[Document.Blob],
    can_import: Document.Node.Name => Boolean,
    node_name: Document.Node.Name,
    span: Command_Span.Span
  ): Blobs_Info = {
    span.name match {
      // inlined errors
      case Thy_Header.THEORY =>
        val reader = span.content_reader
        val header = resources.check_thy(node_name, span.content_reader)
        val imports_pos = header.imports_pos
        val raw_imports =
          try {
            val read_imports = Thy_Header.read(node_name, reader).imports.map(_._1)
            if (imports_pos.length == read_imports.length) read_imports else error("")
          }
          catch { case _: Throwable => List.fill(header.imports.length)("") }

        val errors =
          for { ((import_name, pos), s) <- imports_pos zip raw_imports if !can_import(import_name) }
          yield {
            val completion =
              if (Thy_Header.is_base_name(s)) resources.complete_import_name(node_name, s) else Nil
            "Bad theory import " +
              Markup.Path(import_name.node).markup(quote(import_name.toString)) +
              Position.here(pos) + Completion.report_theories(pos, completion)
          }
        Blobs_Info.errors(errors)

      // auxiliary files
      case _ =>
        val loaded_files = span.loaded_files(syntax)
        val blobs =
          loaded_files.files.map(file =>
            (Exn.capture {
              val src_path = Path.explode(file)
              val name = Document.Node.Name(resources.append(node_name.master_dir, src_path))
              val content = get_blob(name).map(blob => (blob.bytes.sha1_digest, blob.chunk))
              Blob(name, src_path, content)
            }).user_error)
        Blobs_Info(blobs, index = loaded_files.index)
    }
  }

  def build_blobs_info(
    syntax: Outer_Syntax,
    node_name: Document.Node.Name,
    load_commands: List[Command_Span.Span]
  ): Blobs_Info = {
    val blobs =
      for {
        span <- load_commands
        file <- span.loaded_files(syntax).files
      } yield {
        (Exn.capture {
          val dir = node_name.master_dir_path
          val src_path = Path.explode(file)
          val name = Document.Node.Name((dir + src_path).expand.implode_symbolic)
          Blob.read_file(name, src_path)
        }).user_error
      }
    Blobs_Info(blobs)
  }
}


final class Command private(
  val id: Document_ID.Command,
  val node_name: Document.Node.Name,
  val blobs_info: Command.Blobs_Info,
  val span: Command_Span.Span,
  val source: String,
  val init_results: Command.Results,
  val init_markups: Command.Markups
) {
  override def toString: String = id.toString + "/" + span.kind.toString


  /* classification */

  def is_proper: Boolean = span.kind.isInstanceOf[Command_Span.Command_Span]
  def is_ignored: Boolean = span.kind == Command_Span.Ignored_Span

  def is_undefined: Boolean = id == Document_ID.none
  val is_unparsed: Boolean = span.content.exists(_.is_unparsed)
  val is_unfinished: Boolean = span.content.exists(_.is_unfinished)

  def potentially_initialized: Boolean = span.name == Thy_Header.THEORY


  /* blobs */

  def blobs: List[Exn.Result[Command.Blob]] = blobs_info.blobs
  def blobs_index: Int = blobs_info.index

  def blobs_ok: Boolean =
    blobs.forall({ case Exn.Res(_) => true case _ => false })

  def blobs_names: List[Document.Node.Name] =
    for (Exn.Res(blob) <- blobs) yield blob.name

  def blobs_undefined: List[Document.Node.Name] =
    for (Exn.Res(blob) <- blobs if blob.content.isEmpty) yield blob.name

  def blobs_defined: List[(Document.Node.Name, SHA1.Digest)] =
    for (Exn.Res(blob) <- blobs; (digest, _) <- blob.content) yield (blob.name, digest)

  def blobs_changed(doc_blobs: Document.Blobs): Boolean =
    blobs.exists({ case Exn.Res(blob) => doc_blobs.changed(blob.name) case _ => false })


  /* source chunks */

  val chunk: Symbol.Text_Chunk = Symbol.Text_Chunk(source)

  val chunks: Map[Symbol.Text_Chunk.Name, Symbol.Text_Chunk] =
    ((Symbol.Text_Chunk.Default -> chunk) ::
      (for (Exn.Res(blob) <- blobs; (_, file) <- blob.content)
        yield blob.chunk_file -> file)).toMap

  def length: Int = source.length
  def range: Text.Range = chunk.range

  val core_range: Text.Range =
    Text.Range(0,
      span.content.reverseIterator.takeWhile(_.is_ignored).foldLeft(length)(_ - _.source.length))

  def source(range: Text.Range): String = range.substring(source)


  /* theory parents */

  def theory_parents(resources: Resources): List[Document.Node.Name] =
    if (span.name == Thy_Header.THEORY) {
      try {
        val header = Thy_Header.read(node_name, span.content_reader)
        for ((s, _) <- header.imports)
        yield {
          try { resources.import_name(node_name, s) }
          catch { case ERROR(_) => Document.Node.Name.empty }
        }
      }
      catch { case ERROR(_) => Nil }
    }
    else Nil


  /* reported positions */

  def reported_position(
    pos: Position.T
  ) : Option[(Document_ID.Generic, Symbol.Text_Chunk.Name, Option[Symbol.Range])] = {
    pos match {
      case Position.Id(id) =>
        val chunk_name =
          pos match {
            case Position.File(name) if name != node_name.node =>
              Symbol.Text_Chunk.File(name)
            case _ => Symbol.Text_Chunk.Default
          }
        Some((id, chunk_name, Position.Range.unapply(pos)))
      case _ => None
    }
  }

  def message_positions(
    self_id: Document_ID.Generic => Boolean,
    chunk_name: Symbol.Text_Chunk.Name,
    chunk: Symbol.Text_Chunk,
    message: XML.Elem
  ): Set[Text.Range] = {
    def elem(props: Properties.T, set: Set[Text.Range]): Set[Text.Range] =
      reported_position(props) match {
        case Some((id, name, reported_range)) if self_id(id) && name == chunk_name =>
          val opt_range =
            reported_range orElse {
              if (name == Symbol.Text_Chunk.Default)
                Position.Range.unapply(span.position)
              else None
            }
          opt_range match {
            case Some(symbol_range) =>
              chunk.incorporate(symbol_range) match {
                case Some(range) => set + range
                case _ => set
              }
            case None => set
          }
        case _ => set
      }
    def tree(set: Set[Text.Range], t: XML.Tree): Set[Text.Range] =
      t match {
        case XML.Wrapped_Elem(Markup(name, props), _, body) =>
          body.foldLeft(if (Rendering.position_elements(name)) elem(props, set) else set)(tree)
        case XML.Elem(Markup(name, props), body) =>
          body.foldLeft(if (Rendering.position_elements(name)) elem(props, set) else set)(tree)
        case XML.Text(_) => set
      }

    val set = tree(Set.empty, message)
    if (set.isEmpty) elem(message.markup.properties, set)
    else set
  }


  /* accumulated results */

  val init_state: Command.State =
    Command.State(this, results = init_results, markups = init_markups)

  val empty_state: Command.State = Command.State(this)
}