All Downloads are FREE. Search and download functionalities are using the official Maven repository.

untemplate.transpile.scala Maven / Gradle / Ivy

The newest version!
package untemplate

import java.io.{Writer,StringWriter}
import scala.collection.{immutable,mutable}
import scala.util.matching.Regex
import com.mchange.sc.v2.literal.StringLiteral.formatAsciiScalaStringLiteral

import com.mchange.codegenutil.*

// this code is extremely naive and inelegant.
// There must be a more concise and prettier way.
// Maybe someday

private val DefaultInputType            = "immutable.Map[String,Any]"
private val DefaultInputDefaultArg      = "immutable.Map.empty"
private val DefaultOutputMetadataType   = "Nothing"
private val BackstopInputNameIdentifier = toIdentifier("input")
private val DefaultOutputTransformer    = "identity"

private val K128 = 128 * 1024
private val K16  =  16 * 1024


private final case class TranspileData1(source : UntemplateSource, spaceNormalized : Vector[String], indentLevels : Vector[Int], earlyWarnings : List[UntemplateWarning])
private final case class TranspileData2(last : TranspileData1, hasHeader : Boolean, mbInputName : Option[String], mbInputType : Option[String], mbInputDefaultArg : Option[String], mbOutputMetadataType : Option[String], mbOverrideUntemplateName : Option[String], mbHeaderNote : Option[String], textBlockInfos : Vector[TextBlockInfo])
private final case class TranspileData3(last : TranspileData2, headerInfo : Option[HeaderInfo], nonheaderBlocks : Vector[ParseBlock])

private case class HeaderInfo(mbInputName : Option[Identifier], mbInputType : Option[String], mbInputDefaultArg : Option[String], mbOutputMetadataType : Option[String], mbOverrideUntemplateName : Option[Identifier], mbHeaderNote : Option[String], headerBlock : ParseBlock.Code)
private final case class TextBlockInfo(functionName : Option[String], startDelimiter : Option[Int], stopDelimiter : Option[Int])

private object ParseBlock:
  final case class Text( functionIdentifier : Option[Identifier], rawTextBlock : String) extends ParseBlock
  final case class Code( text : String, lastIndent : Int ) extends ParseBlock
private sealed trait ParseBlock

private def prefixTabSpaceToSpaces(spacesPerTab : Int, line : String) : String =
  def tabspace(c : Char) = c == '\t' || c == ' '
  val (tabs, rest) = line.span(tabspace)
  val untab = " " * spacesPerTab
  def replace( b : Byte ) : String =
    if b == '\t' then untab
    else if b == ' ' then " "
    else throw new AssertionError(s"Huh? We should only be replacing spaces and tabs, found byte ${b}")
  val spacified = tabs.getBytes(scala.io.Codec.UTF8.charSet).map(replace).mkString
  spacified + rest

// linenum should be user-interpretable, ie one indexed
private def checkDelimiter(linenum : Int, delimType : String, delimUnanchoredRegex : Regex, delimFullLineRegex : Regex, line : String) : Option[String] =
  delimUnanchoredRegex.findFirstMatchIn(line) match
    case Some( m ) => // okay, we have a delimiter
      if delimFullLineRegex.matches(line) then
        None // okay, a normal good delimiter
      else
        if m.start(0) == 0 then // uh oh, this is in delimiter position but not a valid delimiter line, an error
          throw new ParseException(s"Line ${linenum}: Invalid ${delimType} delimiter line, bad stuff to the right of delimiter.")
        else
          // no linenum in String because we prepend it later.
          Some(s"Unescaped ${delimType} delimiter not at beginning-of-line (col: ${m.start(0) + 1}) will not be recognized as a delimiter! ['${m.group(0)}']")
    case None => // No delimiter to worry about
      None

// linenum should be user-interpretable, ie one indexed
private def checkTextStartDelimiter(linenum : Int, line : String) = checkDelimiter(linenum, "text-start", WarningUnanchoredTextStartDelimiterRegex, AnchoredTextStartDelimiterRegex, line)
private def checkTextEndDelimiter(linenum : Int, line : String) = checkDelimiter(linenum, "text-end", WarningUnanchoredTextEndDelimiterRegex, AnchoredTextEndDelimiterRegex, line)
private def checkHeaderDelimiter(linenum : Int, line : String) = checkDelimiter(linenum, "header", WarningUnanchoredHeaderDelimiterRegex, AnchoredHeaderDelimiterRegex, line)

// linenum should be user-interpretable, ie one indexed
private def earlyValidate( linenum : Int, line : String ) : List[UntemplateWarning] =
  var out = List.empty[UntemplateWarning]

  def addWarning( s : String ) =
    out = toUntemplateWarning(s"Line ${linenum}: ${s}") :: out

  def mbAddWarning( mbs : Option[String]) = mbs.foreach( addWarning )

  val initialWhitespace = line.takeWhile( c => c == ' ' || c == '\t')
  if (initialWhitespace.distinct.length > 1)
    addWarning("Mixes tabs and spaces in initial whitespace, asking for trouble.")

  mbAddWarning( checkTextStartDelimiter(linenum, line) )
  mbAddWarning( checkTextEndDelimiter(linenum, line) )
  mbAddWarning( checkHeaderDelimiter(linenum, line) )

  out

private def untabCountSpacesValidate( gs : UntemplateSource )(using ui : UnitIndent) : TranspileData1 =
  val indents = Array.ofDim[Int](gs.lines.length)
  val oldLines = gs.lines
  val newLines = mutable.Buffer.empty[String]
  var earlyWarnings = List.empty[UntemplateWarning]
  for( i <- 0 until gs.lines.length )
    val oldLine = oldLines(i)
    earlyWarnings = earlyValidate(i+1, oldLine) ::: earlyWarnings
    val untabbed = prefixTabSpaceToSpaces(ui.toInt, oldLine)
    val indent = untabbed.takeWhile(_ == ' ').length
    newLines.append(untabbed)
    indents(i) = indent
  TranspileData1(gs, newLines.toVector, indents.toVector, earlyWarnings.reverse)

private object LineDelimiter:
  // object Header:
  //   def apply(str: String) : Header = if str == null || str.isEmpty then Header(None) else Header(Some(str))
  case class Header(mbInputName : Option[String], mbInputType : Option[String], mbInputDefaultArg : Option[String], mbOutputMetadataType : Option[String], mbOverrideUntemplateName : Option[String], mbHeaderNote : Option[String]) extends LineDelimiter
  object Start:
    def apply(str : String) : Start = if str == null || str.isEmpty then Start(None) else Start(Some(str))
  case class Start(functionName : Option[String]) extends LineDelimiter
  case object End extends LineDelimiter
private sealed trait LineDelimiter

private def carveAroundDelimiterChar(maybeNullOrBlank : String, delimiter : Char, trim : Boolean) : Tuple2[Option[String],Option[String]] =
  val raw =
    if maybeNullOrBlank == null || maybeNullOrBlank.isEmpty then
      (None, None)
    else
      val dotIndex = maybeNullOrBlank.indexOf(delimiter)
      val len = maybeNullOrBlank.length
      val Last = len - 1
      dotIndex match
        case -1 => (Some(maybeNullOrBlank), None)
        case 0 => (None, Some(maybeNullOrBlank.substring(1)))
        case Last => (Some(maybeNullOrBlank.substring(0, len - 1)), None)
        case i => (Some(maybeNullOrBlank.substring(0, i)), Some(maybeNullOrBlank.substring(i + 1)))
  if trim then Tuple2(nonEmptyStringOption(raw(0).map(_.trim)),nonEmptyStringOption(raw(1).map(_.trim))) else raw

private def isBlank( s : String ) = s == null || s.trim.isEmpty

private def basicParse( td1 : TranspileData1 ) : TranspileData2 =
  var headerTuple : Option[Tuple2[Int,LineDelimiter.Header]] = None
  val parseTuples : mutable.SortedMap[Int,LineDelimiter] = mutable.SortedMap.empty // empty only of there are no delimiters at all

  for( i <- 0 until td1.indentLevels.length) // indentLevels.length is also the line length
    if td1.indentLevels(i) == 0 then
      td1.source.lines(i) match {
        case hd @ AnchoredHeaderDelimiterRegex(inputNameType, outputMetadataType, untemplateNameDotFunctionName, headerNote) =>
          // println("SAW HEADER DELIMETER: " + hd)
          val (overrideUntemplateName, functionName) = carveAroundDelimiterChar(untemplateNameDotFunctionName, '.', trim = true)
          val (inputName, inputTypeWithMbDefaultArg) = carveAroundDelimiterChar(inputNameType, ':', trim = true)
          val (inputType, inputDefaultArg)           = carveAroundDelimiterChar(inputTypeWithMbDefaultArg.getOrElse(""), '=', trim = true)
          val mbHeaderNote = if (isBlank(headerNote)) None else Some(headerNote.trim)
          if headerTuple == None then
            headerTuple = Some(Tuple2(i, LineDelimiter.Header(nonEmptyStringOption(inputName), nonEmptyStringOption(inputType), nonEmptyStringOption(inputDefaultArg), nonEmptyStringOption(outputMetadataType), overrideUntemplateName, mbHeaderNote)))
            parseTuples += Tuple2(i, LineDelimiter.Start(functionName))
          else
            throw new ParseException(s"${td1.source.provenance}: Duplicate header delimiter at line ${i}")
        case AnchoredTextStartDelimiterRegex(functionName, extra) =>
          if (!isBlank(extra))
            throw new ParseException(s"${td1.source.provenance}: Text start delimiter line must be blank after delimiter, contains '${extra}'")
          parseTuples += Tuple2(i, LineDelimiter.Start(nonEmptyStringOption(functionName)))
        case AnchoredTextEndDelimiterRegex(extra) =>
          if (!isBlank(extra))
            throw new ParseException(s"${td1.source.provenance}: Text end delimiter line must be blank after delimiter, contains '${extra}'")
          parseTuples += Tuple2(i, LineDelimiter.End)
        case _ => /* No match, move on */
      }

  // sanity checks
  headerTuple.foreach { case (line, ldh) =>
    if line != parseTuples.keys.head then
      throw new ParseException(
        s"${td1.source.provenance}: The first start tuple is at (zero-indexed) line ${parseTuples.keys.head}, but header boundary is at ${line}, should be identical." +
        "Perhaps there is a start delimiter above the header delimiter. That would be bad!"
      )
  }

  // check correct alternation of types
  var lastSeen : LineDelimiter = null; // so sue me... purely an internal implementation detail
  parseTuples.foreach { case (i, delim) =>
    // println( s"Line: ${i+1}     delim: ${delim}" )
    if lastSeen == null then
      lastSeen = delim
    else
      (lastSeen, delim) match {
        case (a : LineDelimiter.Start, b : LineDelimiter.End.type)         => /* Good */
        case (a : LineDelimiter.End.type, b : LineDelimiter.Start)         => /* Good */
        case (a : LineDelimiter.Start, b : LineDelimiter.Start)            =>
          throw new ParseException(s"${td1.source.provenance}: Line ${i+1}: Text region start requested within already started text region. Please escape untemplate delimiters in text.")
        case (a : LineDelimiter.End.type, b : LineDelimiter.End.type)       =>
          throw new ParseException(s"${td1.source.provenance}: Line ${i+1}: Text region end requested within Scala code region.")
        case (_ : LineDelimiter.Header, _) | (_, _ : LineDelimiter.Header) =>
          throw new AssertionError(s"${td1.source.provenance}: Line ${i+1}: There should be no LineDelimiter.Header in parseTuples!")
      }
    lastSeen = delim
  }

  // okay... apparently we have alternating sections. Let's build our output
  val (mbInputName : Option[String], mbInputType : Option[String], mbInputDefaultArg : Option[String], mbOutputMetadataType : Option[String], mbOverrideUntemplateName : Option[String], mbHeaderNote : Option[String]) =
    headerTuple match
      case Some(tup) =>
        val ldHeader = tup(1)
        (ldHeader.mbInputName, ldHeader.mbInputType, ldHeader.mbInputDefaultArg, ldHeader.mbOutputMetadataType, ldHeader.mbOverrideUntemplateName, ldHeader.mbHeaderNote)
      case None =>
        (None, None, None, None, None, None)

  if parseTuples.nonEmpty then
    val textBlockInfos = Vector.newBuilder[TextBlockInfo]

    val headTuple = parseTuples.head
    val (groupTuples, prependHead) =
      headTuple match {
        case (_, _ : LineDelimiter.Start)    => Tuple2(parseTuples, false)
        case (_, _ : LineDelimiter.End.type) => Tuple2(parseTuples.tail, true)
        case (_, _) =>
          throw new AssertionError("There should be no LineDelimter.Header values in parseTuples.")
      }
    if prependHead then
      textBlockInfos.addOne(TextBlockInfo(None,None,Some(headTuple(0))))
    groupTuples.grouped(2).foreach { minimap =>
      val l = minimap.toList
      val (start, functionName) =
        l.head match {
          case (s, LineDelimiter.Start(fname)) => (s, fname)
          case (a,b) =>
            throw new AssertionError(s"Expected tuple of (Int,LineDelimiter.Start), found (${a},${b})")
        }
      val end =
        if (l.tail.nonEmpty) Some(l.tail.head(0))
        else None
      textBlockInfos.addOne(TextBlockInfo(functionName,Some(start),end))
    }
    TranspileData2( td1, headerTuple.nonEmpty, mbInputName, mbInputType, mbInputDefaultArg, mbOutputMetadataType, mbOverrideUntemplateName, mbHeaderNote, textBlockInfos.result() )
  else
    TranspileData2( td1, false, None, None, None, None, None, None, Vector.empty )

private def parseBlockTextFromInfo( unmodifiedLines : Vector[String], info : TextBlockInfo ) =
  val text = (info.startDelimiter, info.stopDelimiter) match
    case (Some(before), Some(until)) => unmodifiedLines.slice(before + 1, until).mkString(LineSep)
    case (None,         Some(until)) => unmodifiedLines.slice(0, until).mkString(LineSep)
    case (Some(before), None       ) => unmodifiedLines.slice(before + 1, unmodifiedLines.size).mkString(LineSep)
    case (None,         None       ) => unmodifiedLines.mkString(LineSep)
  val functionName = info.functionName.map(toIdentifier)
  ParseBlock.Text(functionName, text)

private def collectBlocksNonEmpty( td2 : TranspileData2 ) : TranspileData3 =
  val mbInputNameIdentifier                  = td2.mbInputName.map( toIdentifier )
  val mbInputType                            = td2.mbInputType
  val mbInputDefaultArg                      = td2.mbInputDefaultArg
  val mbOutputMetadataType                   = td2.mbOutputMetadataType
  val mbOverrideUntemplateNameIdentifier     = td2.mbOverrideUntemplateName.map( toIdentifier )
  val mbHeaderNote                           = td2.mbHeaderNote
  var headerBlock : Option[ParseBlock.Code]  = None

  // for text, we take from unmodified UntemplateSource.
  // in other words, don't mess with tabs and spaces
  val unmodifiedLines = td2.last.source.lines

  // for code, we take from our tab-to-space-normalized lines
  val normalizedLines = td2.last.spaceNormalized
  val indents = td2.last.indentLevels

  val infos = td2.textBlockInfos
  var lastInfo : Option[TextBlockInfo] = None
  val blocksBuilder = Vector.newBuilder[ParseBlock]

  def registerCodeBlock(codeBlock: ParseBlock.Code) =
    (td2.hasHeader, headerBlock) match
      case (true, None) => headerBlock = Some(codeBlock)
      case _ => blocksBuilder.addOne(codeBlock)

  infos.foreach { info =>
    val mbPriorCodeBlock =
      (lastInfo.flatMap(_.stopDelimiter), info.startDelimiter) match
        case (Some(before), Some(until)) => Some(ParseBlock.Code(normalizedLines.slice(before+1,until).mkString("",LineSep,LineSep), if until == 0 then 0 else indents(until-1)))
        case (None,         Some(until)) => Some(ParseBlock.Code(normalizedLines.slice(0, until).mkString("",LineSep,LineSep), 0))
        case (Some(before), None       ) => throw new AssertionError(s"Interior text blocks should have start delimteres! [prior: ${lastInfo}, current: ${info}]")
        case (None,         None       ) => None // this is the first info, no start means we begin inside text

    mbPriorCodeBlock.foreach(registerCodeBlock)
    blocksBuilder.addOne(parseBlockTextFromInfo(unmodifiedLines, info))
    lastInfo = Some(info)
  }
  val mbLastCodeBlock =
    lastInfo.flatMap( _.stopDelimiter ).map { before =>
      ParseBlock.Code(normalizedLines.slice(before+1,normalizedLines.length).mkString("",LineSep,LineSep), indents.last )
    }
  mbLastCodeBlock.foreach(blocksBuilder.addOne)

  val nonheaderBlocks = blocksBuilder.result()
  val mbHeaderInfo =
    headerBlock.map(hblock => HeaderInfo(mbInputNameIdentifier, mbInputType, mbInputDefaultArg, mbOutputMetadataType, mbOverrideUntemplateNameIdentifier, mbHeaderNote, hblock))

  TranspileData3( td2, mbHeaderInfo : Option[HeaderInfo], nonheaderBlocks )

private def collectBlocks( td2 : TranspileData2 ) : TranspileData3 =
  if (td2.textBlockInfos.nonEmpty) then collectBlocksNonEmpty(td2)
  else
    val unmodifiedText = td2.last.source.lines.mkString(LineSep)
    TranspileData3(td2, None, Vector(ParseBlock.Text(None, unmodifiedText)))

private def unescapeUntemplateDelimeters( s : String ) : String =
  UnescapeRegexes.foldLeft(s)( (last, regex) => regex.replaceAllIn(last, m => m.group(1) ) )

private def rawTextToSourceConcatenatedLiteralsAndExpressions( text : String ) : String =
  val sb = new StringBuilder(text.length * 2)
  val mi = EmbeddedExpressionRegex.findAllIn(text)
  var nextStart = 0
  while mi.hasNext do
    mi.next()
    val nextEnd = mi.start
    val expression = mi.group(1)
    val textBit = text.substring(nextStart, nextEnd)
    val unescapedTextBit = unescapeUntemplateDelimeters(textBit)
    sb.append(formatAsciiScalaStringLiteral(unescapedTextBit))
    sb.append(" + (")
    sb.append( expression )
    sb.append(") +")
    sb.append(LineSep)
    nextStart = mi.end
  val lastTextBit = text.substring(nextStart)
  val unescapedLastTextBit = unescapeUntemplateDelimeters(lastTextBit)
  sb.append(formatAsciiScalaStringLiteral( unescapedLastTextBit + LineSep)) //we removed the separators parsing into lines, better put 'em back!
  sb.toString

private def rawTextToBlockPrinter( innerIndent : Int, text : String ) : String =
  // we used to generate a function accepting ${inputName} : ${inputIdentifier}, but we have this anyway
  // in the closure, so there's no point in complicating things with a shadow
  val spaces = " " * innerIndent
  val stringExpression = rawTextToSourceConcatenatedLiteralsAndExpressions( text )
  s"""|new Function0[String]:
      |${spaces}def apply() : String =
      |${increaseIndent(innerIndent*2)(stringExpression)}""".stripMargin

private def rawTextToBlockPrinter( text : String )(using ui : UnitIndent) : String = rawTextToBlockPrinter( ui.toInt, text )

private final case class PartitionedHeaderBlock(packageOverride : Option[String], importsText : String, constructorText : String, otherHeaderText : String, otherLastIndent : Int)

private def partitionHeaderBlock( td1 : TranspileData1, text : String )(using ui : UnitIndent) : PartitionedHeaderBlock =
  // println( s">>> headerBlockToPartition: ${text}" )

  val srcId = td1.source.provenance

  val (importsLines,     other0) = text.linesIterator.to(List).partition( _.trim.startsWith("import ") )
  val (packageLines,     other1) = other0.partition( _.trim.startsWith("package ") )
  val (constructorLines, other2) = other1.partition( _.startsWith(">") ) // no trim. only '>' as first character

  val importsText = importsLines.map( _.trim ).mkString("",LineSep,LineSep)

  val packageOverride =
    if packageLines.nonEmpty then
      val packageComponents =
        packageLines.map {
          case PackageExtractFromLineRegex(pkgPath) => pkgPath
          case line                                 => throw new ParseException(s"${td1.source.provenance}: Bad package declaration in header: '${line}''")
        }
      Some( joinPackageIdentifierPaths(packageComponents) )
    else
      None

  val constructorText =
    if constructorLines.nonEmpty then
      val (constructorTabs, constructorLinesUntrimmed) = constructorLines.map( _.span( _ == '>') ).unzip //.mkString("",LineSep,LineSep)
      if constructorTabs.distinct.size > 1 then
        throw new ParseException(s"${srcId}: Constructor lines in the header must each begin with the same number of '>' characters.")
      else
        val margins = constructorLinesUntrimmed.map( _.takeWhile(c => c ==' ' || c =='\t') )
        val isBlanks = constructorLinesUntrimmed.map( _.trim.isEmpty )
        val marginChars = margins.mkString.distinct
        if marginChars.size > 1 then
          throw new ParseException(s"${srcId}: In constructor text in header, whitespace margins after '>' must not mix spaces and tabs, does.")
        else
          val normalizedMargins =
            if marginChars.size == 1 && marginChars(0) == '\t' then
              val tabspaces = (" " * ui.toInt)
              margins.map( tabs => tabspaces * tabs.length )
            else
              margins
          //val minMargin = normalizedMargins.map( _.length).reduceLeft( math.min )

          // grr... we don't want blank-after-'>' lines to mess with our margins
          val normalizedMarginsIsBlanks = normalizedMargins.zip( isBlanks )
          val minRealMargin =
            normalizedMarginsIsBlanks.foldLeft(Int.MaxValue){ case (accum, (nm, ib)) => if ib then accum else math.min(accum, nm.length) }
          val trimmedMargins = normalizedMarginsIsBlanks.map{ case (nm, ib) => if ib then "" else nm.substring(minRealMargin) }
          val constructorTextLines = trimmedMargins.zip( constructorLinesUntrimmed.map( _.trim ) ).map(tup => tup(0) + tup(1))
          constructorTextLines.mkString("",LineSep,LineSep)
    else
      LineSep

  val otherHeaderText = other2.mkString("",LineSep,LineSep)
  val otherLastIndent = other2.lastOption.fold( 0 )( _.takeWhile(_ == ' ').length )
  PartitionedHeaderBlock(packageOverride, importsText, constructorText, otherHeaderText, otherLastIndent)

private def transpileToWriter (
  locationPackage       : LocationPackage,
  defaultUntemplateName : Identifier,
  selectCustomizer      : Customizer.Selector,
  src                   : UntemplateSource,
  srcIdentifier         : Option[String],
  w                     : Writer,
  warnings              : mutable.Buffer[UntemplateWarning]
) : (Option[String], Identifier) =
  val td1 = untabCountSpacesValidate( src )
  val td2 = basicParse( td1 )
  val td3 = collectBlocks( td2 )

  warnings ++= td1.earlyWarnings

  val (mbInputName, mbInputType, mbInputDefaultArg, mbOutputMetadataType, mbOverrideUntemplateName, mbHeaderNote, mbPartitionedHeaderBlock) =
    td3.headerInfo match
      case Some( HeaderInfo( mbInputName, mbInputType, mbInputDefaultArg, mbOutputMetadataType, mbOverrideUntemplateName, mbHeaderNote, headerBlock ) ) => (mbInputName, mbInputType, mbInputDefaultArg, mbOutputMetadataType, mbOverrideUntemplateName, mbHeaderNote, Some(partitionHeaderBlock(td1, headerBlock.text)))
      case None                                                                                                                                         => (None, None, None, None, None, None, None)

  val mbFromLocationPackage = if locationPackage.nonDefault then Some(locationPackage.dotty) else None
  val mbExplicitPackage = mbPartitionedHeaderBlock.flatMap(_.packageOverride)
  val resolvedPackage = (mbExplicitPackage orElse mbFromLocationPackage)

  val tentativeOutputMetadataType: String = (mbOutputMetadataType).getOrElse(DefaultOutputMetadataType)
  val resolvedUntemplateName: Identifier = (mbOverrideUntemplateName).getOrElse(defaultUntemplateName)

  val customizerKey      = Customizer.Key (
    inferredPackage      = mbFromLocationPackage.getOrElse(""),
    resolvedPackage      = resolvedPackage.getOrElse(""),
    inferredFunctionName = defaultUntemplateName.toString,
    resolvedFunctionName = resolvedUntemplateName.toString,
    outputMetadataType   = tentativeOutputMetadataType,
    headerNote           = mbHeaderNote.getOrElse(""),
    sourceIdentifier     = srcIdentifier
  )
  val customizer = selectCustomizer( customizerKey )

  val inputName : Identifier = (mbInputName orElse customizer.mbDefaultInputName.map(asIdentifier)).getOrElse( BackstopInputNameIdentifier )

  val ( inputType, mbDefaultArg ) =
    val explicit   = mbInputType.map(tpe => Tuple2(tpe, mbInputDefaultArg))
    val customized = (customizer.mbDefaultInputTypeDefaultArg.map(ditda => Tuple2(ditda.inputType, ditda.mbDefaultArg)))
    val backstop   = Tuple2(DefaultInputType, Some(DefaultInputDefaultArg))
    (explicit orElse customized).getOrElse( backstop )

  val inputDefaultArgClause = mbDefaultArg.fold("")( defaultArg => " = " + defaultArg )

  // None is the default package. Customizers specify that as an empty String, so we take care to convert that to None
  val mbPackagePath = mbExplicitPackage orElse nonEmptyStringOption(customizer.mbOverrideInferredPackage) orElse mbFromLocationPackage

  val untemplateName = (mbOverrideUntemplateName orElse customizer.mbOverrideInferredFunctionName.map(asIdentifier)).getOrElse(defaultUntemplateName)

  val perhapsCustomizedOutputMetadataType = (mbOutputMetadataType orElse customizer.mbDefaultMetadataType).getOrElse(DefaultOutputMetadataType)

  //println(">>>>> perhapsCustomizedOutputMetadataType: " + perhapsCustomizedOutputMetadataType)

  val defaultMetadataValue =
    customizer.mbDefaultMetadataValue match
      case Some(expr) =>
        if expr.startsWith("Some") || expr == "None" then
          val warning = toUntemplateWarning (
            s"${untemplateName}: Customizer override of the default metadata value '${expr}' should not be 'None' or wrapped in Some(...) unless the metadata type is itself an option!"
          )
          warnings += warning
        s"Some(${expr})"
      case None => "None"

  val defaultOutputTransformer = customizer.mbDefaultOutputTransformer.getOrElse(DefaultOutputTransformer)

  val textBlocks = td3.nonheaderBlocks.collect { case b : ParseBlock.Text => b }

  // start writing!
  w.writeln(autogeneratedComment(srcIdentifier))

  // fragile, at-most-one single line, package declaration relied upon for indexing
  // see UntemplateScala.fromScalaText
  mbPackagePath.foreach { dotpath =>
    w.writeln(s"package ${dotpath}")
    w.writeln()
  }
  w.writeln("import java.io.{Writer,StringWriter}")
  w.writeln("import scala.collection.{immutable,mutable}")
  w.writeln()
  if (customizer.extraImports.nonEmpty)
    customizer.extraImports.foreach { line =>
      val tl = line.trim
      if tl.startsWith("import") then
        w.writeln(tl)
      else
        w.writeln(s"import ${tl}")
    }
    w.writeln()

  mbPartitionedHeaderBlock.foreach { phb =>
    if phb.importsText.nonEmpty then
      w.write(phb.importsText) // already LineSep terminated
      w.writeln()
  }
  val blockPrinterTups =
    (for (i <- 0 until textBlocks.length) yield (s"block${i}", textBlocks(i).functionIdentifier, rawTextToBlockPrinter( textBlocks(i).rawTextBlock ))).toVector

  val argList = s"(${inputName} : ${inputType}${inputDefaultArgClause})"
  val fullReturnType = s"untemplate.Result[${perhapsCustomizedOutputMetadataType}]"
  val embeddableDefaultArg = mbDefaultArg.fold(s"(None : Option[${inputType}])")(defaultArg => s"""Some(${defaultArg})""")

  // fragile, indexes depend on this naming convention for the function object
  // fragile, next two lines relied upon for indexing, see UntemplateScala.fromScalaText(...)
  val functionObjectName = s"Untemplate_${untemplateName}"
  w.indentln(0)(s"val ${functionObjectName} = new untemplate.Untemplate[${inputType},${perhapsCustomizedOutputMetadataType}]:")
  w.indentln(1)(s"""val UntemplateFunction                    : untemplate.Untemplate[${inputType},${perhapsCustomizedOutputMetadataType}] = this""")
  w.indentln(1)(s"""val UntemplateName                        : String = "${untemplateName}"""")
  w.indentln(1)(s"""val UntemplatePackage                     : String = "${mbPackagePath.getOrElse("")}"""")
  w.indentln(1)(s"""val UntemplateInputName                   : String = "${inputName}"""")
  w.indentln(1)(s"""val UntemplateInputTypeDeclared           : String = "${inputType}"""")
  w.indentln(1)(s"""val UntemplateInputTypeCanonical          : Option[String] = untemplate.Macro.nonEmptyStringOption( untemplate.Macro.recursiveCanonicalName[${inputType}] )""")
  w.indentln(1)(s"""val UntemplateInputDefaultArgument        : Option[${inputType}] = ${embeddableDefaultArg}""")
  w.indentln(1)(s"""val UntemplateOutputMetadataTypeDeclared  : String = "${perhapsCustomizedOutputMetadataType}"""")
  w.indentln(1)(s"""val UntemplateOutputMetadataTypeCanonical : Option[String] = untemplate.Macro.nonEmptyStringOption( untemplate.Macro.recursiveCanonicalName[${perhapsCustomizedOutputMetadataType}] )""")
  w.indentln(1)(s"""val UntemplateHeaderNote                  : String = "${mbHeaderNote.getOrElse("")}"""")
  w.indentln(1)(s"""val UntemplateLastModified                : Option[Long] = ${src.metadata.flatMap( _.mbLastModMetaOption ).fold("None")(l => "Some("+l+"L)")}""")
  w.indentln(1)( """val UntemplateSynthetic                   : Boolean = false""") // transpiled from UntemplateSource, as what we are transpiling is, are by definition not synthetic
  w.writeln()

  val extraConstructorText =
    val defaultUntemplateAttributesDeclaration = "val UntemplateAttributes : immutable.Map[String,Any] = immutable.Map.empty" + LineSep
    mbPartitionedHeaderBlock match
      case Some(phb) if phb.constructorText.indexOf("UntemplateAttributes") >= 0 =>
        phb.constructorText // already LineSep terminated
      case Some(phb) =>
        defaultUntemplateAttributesDeclaration + phb.constructorText // already LineSep terminated
      case None =>
        defaultUntemplateAttributesDeclaration  // already LineSep terminated

  // println(">>>>>> increasing indent of:")
  // print( extraConstructorText )
  // println("<<<<<<")

  w.write(increaseIndentLevels(1)(extraConstructorText)) // already LineSep terminated
  w.writeln()

  w.indentln(1)(s"def apply${argList} : ${fullReturnType} =")
  w.indentln(2)(untemplateBody(td3, inputName, inputType, perhapsCustomizedOutputMetadataType, defaultMetadataValue, defaultOutputTransformer, blockPrinterTups, mbPartitionedHeaderBlock))
  w.indentln(1)(s"end apply")
  w.indentln(0)(s"end ${functionObjectName}")
  w.writeln()
  w.indentln(0)(s"def ${untemplateName}${argList} : ${fullReturnType} = ${functionObjectName}( ${inputName} )")
  ( mbPackagePath, untemplateName )

private def untemplateBody(
  td3 : TranspileData3,
  inputName : Identifier,
  inputType : String,
  outputMetadataType : String,
  defaultMetadataValue : String,
  defaultOutputTransformer : String,
  blockPrinterTups : Vector[Tuple3[String,Option[Identifier],String]],
  mbPartitionedHeaderBlock : Option[PartitionedHeaderBlock]
)(using ui : UnitIndent) : String =
  val origTextLen = td3.last.last.source.textLen

  val w = new StringWriter(K16) // XXX: Hardcoded initial capacity
  var lastIndentSpaces = 0

  def lastIndentLevel =
    lastIndentSpaces / ui.toInt + (if (lastIndentSpaces % ui.toInt) == 0 then 0 else 1)

  // setup author resources
  // w.writeln(s"import ${helperName}.*")
  // w.writeln()

  // For now I don't think this is worth the extra complexity.
  //
  // w.indentln(0)("extension (s : mutable.Map[String,Any])")
  // w.indentln(1)("def as[T](key: String): T = s(key).asInstanceOf[T]")
  // w.indentln(1)("def check[T](key: String): Option[T] = s.get(key).map(_.asInstanceOf[T])")
  // w.writeln()

  // w.writeln("val scratchpad : mutable.Map[String,Any] = mutable.Map.empty[String,Any]")
  w.writeln(s"val writer             : StringWriter = new StringWriter(${origTextLen*10})")
  w.writeln(s"val attrs              : immutable.Map[String,Any] = UntemplateAttributes")
  w.writeln(s"var mbMetadata         : Option[${outputMetadataType}] = ${defaultMetadataValue}")
  w.writeln(s"var outputTransformer  : Function1[untemplate.Result[${outputMetadataType}],untemplate.Result[${outputMetadataType}]] = ${defaultOutputTransformer}")
  w.writeln()

  // header first
  mbPartitionedHeaderBlock.foreach { phb =>
    // println(s">>> phb: ${phb}")
    // println(s">>> phb.otherHeaderText: ${phb.otherHeaderText}")
    w.writeln( phb.otherHeaderText )
    lastIndentSpaces = phb.otherLastIndent
  }

  var textBlockCount = 0
  td3.nonheaderBlocks.foreach { block =>
    // println(s"Processing block: ${block}")
    block match
      case cblock : ParseBlock.Code =>
        // println(s">>> cblock: ${cblock}")
        // println(s""">> cblock.text.endsWith("\n"): ${cblock.text.endsWith("\n")}""")
        w.indent(0)(cblock.text) // properly includes its trailing line feed
        lastIndentSpaces = cblock.lastIndent
      case tblock : ParseBlock.Text =>
        val tup = blockPrinterTups( textBlockCount )
        tblock.functionIdentifier match
          case Some( fcnName ) =>
            w.indentln(lastIndentLevel)(s"val ${tup(0)} = ${tup(2)}" )
            w.indentln(lastIndentLevel)(s"def ${fcnName}() = ${tup(0)}()" )
          case None =>
            w.indentln(lastIndentLevel + 1)(s"val ${tup(0)} = ${tup(2)}" )
            w.indentln(lastIndentLevel + 1)(s"writer.write(block${textBlockCount}())${LineSep}")
        textBlockCount += 1
  }
  w.indentln(0)("outputTransformer( untemplate.Result( mbMetadata, writer.toString ) )")
  w.toString

private def defaultTranspile(
  locationPackage       : LocationPackage,
  defaultUntemplateName : Identifier,
  selectCustomizer      : Customizer.Selector,
  src                   : UntemplateSource,
  srcIdentifier         : Option[String]
) : UntemplateScala =
  val w = new StringWriter(K16) // XXX: hardcoded initial buffer length, should we examine src?
  val warnings = mutable.Buffer.empty[UntemplateWarning]
  val (mbPkgPath, untemplateName) = transpileToWriter(locationPackage, defaultUntemplateName, selectCustomizer, src, srcIdentifier, w, warnings)
  UntemplateScala(mbPkgPath.getOrElse(""), untemplateName, warnings.to(Vector), w.toString)





© 2015 - 2024 Weber Informatics LLC | Privacy Policy