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

io.kaitai.struct.languages.NimCompiler.scala Maven / Gradle / Ivy

package io.kaitai.struct.languages

import io.kaitai.struct.datatype.DataType._
import io.kaitai.struct.datatype._
import io.kaitai.struct.exprlang.Ast
import io.kaitai.struct.format._
import io.kaitai.struct.languages.components._
import io.kaitai.struct.translators.NimTranslator
import io.kaitai.struct.{ClassTypeProvider, RuntimeConfig, Utils}

class NimCompiler(typeProvider: ClassTypeProvider, config: RuntimeConfig)
  extends LanguageCompiler(typeProvider, config)
    with SingleOutputFile
    with EveryReadIsExpression
    with UpperCamelCaseClasses
    with FixedContentsUsingArrayByteLiteral
    with UniversalFooter
    with AllocateIOLocalVar
    with SwitchIfOps
    with UniversalDoc {

  import NimCompiler._

  // Written from scratch
  def blankLine: Unit = out.puts
  def imports = importList.toList.map((x) => s"import $x").mkString("\n")
  def namespaced(names: List[String]): String = names.map(n => camelCase(n, true)).mkString("_")
  def typeSectionHeader: Unit = {
    out.puts("type")
    out.inc
  }
  def typeSectionFooter: Unit = {
    out.dec
    out.puts
  }
  def instanceForwardDeclaration(className: List[String], instName: InstanceIdentifier, dataType: DataType): Unit = {
    out.puts(s"proc ${idToStr(instName).dropRight(4)}*(this: ${namespaced(className)}): ${ksToNim(dataType)}")
  }
  def fromFile(name: List[String]): Unit = {
    val n = namespaced(name)
    out.puts(s"proc fromFile*(_: typedesc[$n], filename: string): $n =")
    out.inc
    out.puts(s"$n.read(newKaitaiFileStream(filename), nil, nil)")
    out.dec
    out.puts
  }

  override def opaqueClassDeclaration(classSpec: ClassSpec): Unit =
    out.puts("import \"" + classSpec.name.head + "\"")
  override def innerEnums = false
  override val translator: NimTranslator = new NimTranslator(typeProvider, importList)
  override def universalFooter: Unit = {
    out.dec
  }
  override def allocateIO(id: Identifier, rep: RepeatSpec): String = {
    val ioName = s"${idToStr(id)}Io"
    val arg = rep match {
      case NoRepeat => idToStr(id) + "Expr"
      case _ => translator.doName(Identifier.ITERATOR2)
    }
    out.puts(s"let $ioName = newKaitaiStream($arg)")
    ioName
  }

  // Members declared in io.kaitai.struct.languages.components.SingleOutputFile
  override def outImports(topClass: ClassSpec) =
    importList.toList.map((x) => s"import $x").mkString("\n") + "\n\n"

  // Members declared in io.kaitai.struct.languages.components.ExtraAttrs
  // def extraAttrForIO(id: Identifier, rep: RepeatSpec): List[AttrSpec] = ???

  // Members declared in io.kaitai.struct.languages.components.ExceptionNames
  override def ksErrorName(err: KSError): String = "KaitaiError" // TODO: maybe add more debugging info

  // Members declared in io.kaitai.struct.languages.components.LanguageCompiler
  override def importFile(file: String): Unit = {
    importList.add(file)
  }
  override def alignToByte(io: String): Unit = out.puts(s"alignToByte($io)")
  override def attrFixedContentsParse(attrName: Identifier, contents: String): Unit = {
    out.puts(s"this.${idToStr(attrName)} = $normalIO.ensureFixedContents($contents)")
  }
  // def attrParse(attr: AttrLikeSpec, id: Identifier, defEndian: Option[Endianness]): Unit = ???
  override def attrParseHybrid(leProc: () => Unit, beProc: () => Unit): Unit = {
    out.puts("if this.isLe:")
    out.inc
    leProc()
    out.dec
    out.puts("else:")
    out.inc
    beProc()
    out.dec
  }

  // Works but is crappily written; I want to rewrite this later XXX
  override def attrProcess(proc: ProcessExpr, varSrc: Identifier, varDest: Identifier, rep: RepeatSpec): Unit = {
    val srcExpr = rep match {
      case RepeatEos | RepeatExpr(_) | RepeatUntil(_) => privateMemberName(varSrc) + "[i]"
      case NoRepeat => privateMemberName(varSrc)
    }
    val expr = proc match {
      case ProcessXor(xorValue) =>
        s"$srcExpr.processXor(${expression(xorValue)})"
      case ProcessZlib =>
        s"$srcExpr.processZlib()"
      case ProcessRotate(isLeft, rotValue) =>
        val expr = if (isLeft) {
          expression(rotValue)
        } else {
          s"8 - (${expression(rotValue)})"
        }
        s"$srcExpr.processRotateLeft(int($expr))"
      case ProcessCustom(name, args) =>
        val namespace = name.head
        val procPath = name.mkString(".")
        importList.add(namespace)
        s"$procPath($srcExpr, ${args.map(expression).mkString(", ")})"
    }
    handleAssignment(varDest, expr, rep, false)
  }
  override def attributeDeclaration(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {
    out.puts(s"`${idToStr(attrName)}`*: ${ksToNim(attrType)}")
  }
  override def instanceDeclaration(attrName: InstanceIdentifier, attrType: DataType, isNullable: Boolean): Unit = {
    out.puts(s"`${idToStr(attrName)}`: ${ksToNim(attrType)}")
    out.puts(s"`${instanceFlagIdentifier(attrName)}`: bool")
  }
  override def attributeReader(attrName: Identifier, attrType: DataType, isNullable: Boolean): Unit = {}
  override def classConstructorHeader(name: List[String], parentType: DataType, rootClassName: List[String], isHybrid: Boolean, params: List[ParamDefSpec]): Unit = {}
  override def classHeader(name: List[String]): Unit = {
    out.puts(s"${namespaced(name)}* = ref object of KaitaiStruct")
    out.inc
  }
  override def condIfHeader(expr: Ast.expr): Unit = {
    out.puts(s"if ${expression(expr)}:")
    out.inc
  }
  override def classFooter(name: List[String]): Unit = {
    typeProvider.nowClass.meta.endian match {
      case Some(_: CalcEndian) | Some(InheritedEndian) =>
        out.puts(s"${idToStr(EndianIdentifier)}: bool")
      case _ =>
    }
    universalFooter
  }

  override def condRepeatCommonInit(id: Identifier, dataType: DataType, needRaw: NeedRaw): Unit = {
    // sequences don't have to be manually initialized in Nim - they're automatically initialized as
    // empty sequences (see https://narimiran.github.io/nim-basics/#_result_variable)
  }

  override def condRepeatEosHeader(id: Identifier, io: String, dataType: DataType): Unit = {
    out.puts("block:")
    out.inc
    out.puts("var i: int")
    out.puts(s"while not $io.isEof:")
    out.inc
  }
  override def condRepeatEosFooter: Unit = {
    out.puts("inc i")
    out.dec
    out.dec
  }
  override def condRepeatExprHeader(id: Identifier, io: String, dataType: DataType, repeatExpr: Ast.expr): Unit = {
    out.puts(s"for i in 0 ..< int(${expression(repeatExpr)}):")
    out.inc
  }
  override def condRepeatUntilHeader(id: Identifier, io: String, dataType: DataType, untilExpr: Ast.expr): Unit = {
    out.puts("block:")
    out.inc
    out.puts("var i: int")
    out.puts("while true:")
    out.inc
  }
  override def condRepeatUntilFooter(id: Identifier, io: String, dataType: DataType, untilExpr: Ast.expr): Unit = {
    typeProvider._currentIteratorType = Some(dataType)
    out.puts(s"if ${expression(untilExpr)}:")
    out.inc
    out.puts("break")
    out.dec
    out.puts("inc i")
    out.dec
    out.dec
  }
  // For this to work, we need a {.lenientCase.} pragma which disables nim's exhaustive case coverage check
  override def enumDeclaration(curClass: List[String], enumName: String, enumColl: Seq[(Long, EnumValueSpec)]): Unit = {
    val enumClass = namespaced(curClass)
    out.puts(s"${enumClass}_${camelCase(enumName, true)}* = enum")
    out.inc
    enumColl.foreach { case (id, label) =>
      val order = if (s"$id" == "-9223372036854775808") "low(int64)" else s"$id"
      out.puts(s"${label.name} = $order")
    }
    out.dec
  }
//  def enumFooter: Unit = {
//    universalFooter
//    out.puts
//  }
//  def enumTemplate: Unit = {
//    out.puts("template defineEnum(typ) =")
//    out.inc
//    out.puts("type typ* = distinct int64")
//    out.puts("proc `==`*(x, y: typ): bool {.borrow.}")
//    out.dec
//  }
//  def enumTemplateFooter: Unit = out.puts
//  def enumHeader: Unit = {
//    out.puts("const")
//    out.inc
//  }
//  def enumConstantsFooter: Unit = {
//    universalFooter
//    out.puts
//  }
//  def enumConstants(curClass: List[String], enumName: String,  enumColl: Seq[(Long, EnumValueSpec)]): Unit = {
//    val enumClass = namespaced(curClass)
//    enumColl.foreach { case (id: Long, label: EnumValueSpec) =>
//      // This hack is needed because the lowest int64 literal is not supported in Nim
//      val const = if (s"$id" == "-9223372036854775808") "low(int64)" else s"$id"
//      out.puts(s"${label.name}* = ${enumClass}_$enumName($const)") }
//  }
  override def fileHeader(topClassName: String): Unit = {
    importList.add(config.nimModule)
    importList.add("options")
  }
  override def indent: String = "  "
  override def instanceCalculate(instName: Identifier, dataType: DataType, value: Ast.expr): Unit = {
    val cast = s"${ksToNim(dataType)}(${expression(value)})"
    handleAssignmentSimple(instName, cast)
  }
  override def instanceCheckCacheAndReturn(instName: InstanceIdentifier, dataType: DataType): Unit = {
    out.puts(s"if this.${instanceFlagIdentifier(instName)}:")
    out.inc
    out.puts(s"return ${privateMemberName(instName)}")
    out.dec
  }
  override def instanceHeader(className: List[String], instName: InstanceIdentifier, dataType: DataType, isNullable: Boolean): Unit = {
    out.puts(s"proc ${idToStr(instName).dropRight(4)}(this: ${namespaced(className)}): ${ksToNim(dataType)} = ")
    out.inc
  }
  override def instanceFooter = {
    universalFooter
    out.puts
  }
  override def instanceReturn(instName: InstanceIdentifier, attrType: DataType): Unit = {
    out.puts(s"this.${instanceFlagIdentifier(instName)} = true")
    out.puts(s"return ${privateMemberName(instName)}")
  }
  // def normalIO: String = ???
  override def outFileName(topClassName: String): String = s"$topClassName.nim"
  override def pushPos(io: String): Unit = out.puts(s"let pos = $io.pos()")
  override def popPos(io: String): Unit = out.puts(s"$io.seek(pos)")
  override def readFooter(): Unit = {
    universalFooter
    out.puts
  }
  override def readHeader(endian: Option[FixedEndian], isEmpty: Boolean): Unit = {
    val t = namespaced(typeProvider.nowClass.name)
    val p = ksToNim(typeProvider.nowClass.parentType)
    val r = namespaced(typeProvider.topClass.name)
    val paramsArg = Utils.join(typeProvider.nowClass.params.map((p) =>
      s"${paramName(p.id)}: any"
    ), ", ", ", ", "")

    endian match {
      case None =>
        out.puts(s"proc read*(_: typedesc[$t], io: KaitaiStream, root: KaitaiStruct, parent: $p$paramsArg): $t =")
        out.inc
        out.puts("template this: untyped = result")
        out.puts(s"this = new($t)")
        // The cast in the if clause is used to bypass semantic analysis
        // The cast in the else clause should be a normal type conversion instead,
        // but for some reason it doesn't work. Needs further investigation
        out.puts(s"let root = if root == nil: cast[$r](this) else: cast[$r](root)")
        out.puts(s"this.io = io")
        out.puts(s"this.root = root")
        out.puts(s"this.parent = parent")
        typeProvider.nowClass.params.foreach((p) => handleAssignmentSimple(p.id, s"${ksToNim(p.dataType)}(${paramName(p.id)})"))

        typeProvider.nowClass.meta.endian match {
          case Some(_: CalcEndian) =>
            out.puts(s"this.${idToStr(EndianIdentifier)} = false")
          case Some(InheritedEndian) =>
            out.puts(s"this.${idToStr(EndianIdentifier)} = " +
              s"this.${idToStr(ParentIdentifier)}." +
              s"${idToStr(EndianIdentifier)}")
          case _ =>
        }
        out.puts
      case Some(e) =>
        out.puts
        out.puts(s"proc read${camelCase(e.toSuffix, true)}(this: $t) =")
        out.inc
    }
  }
  // def results(topClass: ClassSpec): Map[String, String] = ???
  override def runRead(name: List[String]): Unit = out.puts("read()") // TODO: missing type argument
  override def runReadCalc(): Unit = {
    out.puts
    out.puts("if this.isLe:")
    out.inc
    out.puts("readLe(this)")
    out.dec
    out.puts("else:")
    out.inc
    out.puts("readBe(this)")
    out.dec
  }
  override def seek(io: String, pos: Ast.expr): Unit = out.puts(s"$io.seek(int(${expression(pos)}))")
  override def useIO(ioEx: Ast.expr): String = {
    out.puts(s"let io = ${expression(ioEx)}")
    "io"
  }
  override def classForwardDeclaration(name: List[String]): Unit = {
    val t = namespaced(typeProvider.nowClass.name)
    val p = ksToNim(typeProvider.nowClass.parentType)
    val paramsArg = Utils.join(typeProvider.nowClass.params.map((p) =>
      s"${paramName(p.id)}: any"
    ), ", ", ", ", "")

    out.puts(s"proc read*(_: typedesc[$t], io: KaitaiStream, root: KaitaiStruct, parent: $p$paramsArg): $t")
  }

  // Members declared in io.kaitai.struct.languages.components.ObjectOrientedLanguage
  override def idToStr(id: Identifier): String = {
    id match {
      case IoIdentifier => "io"
      case NamedIdentifier(name) =>  camelCase(name, false)
      case InstanceIdentifier(name) => camelCase(name, false) + "Inst"
      case IoStorageIdentifier(innerId) => "io" + camelCase(idToStr(innerId), true)
      case SpecialIdentifier(name) => camelCase(name, false)
      case NumberedIdentifier(idx) => s"${NumberedIdentifier.TEMPLATE}$idx"
      case RawIdentifier(innerId) => "raw" + camelCase(idToStr(innerId), true)
    }
  }
  override def localTemporaryName(id: Identifier): String = idToStr(id)
  override def privateMemberName(id: Identifier): String = {
    val name = idToStr(id)
    val prefix = "this"
    s"$prefix.$name"
  }
  override def publicMemberName(id: Identifier): String = idToStr(id)

  // Members declared in io.kaitai.struct.languages.components.EveryReadIsExpression
  override def bytesPadTermExpr(expr0: String, padRight: Option[Int], terminator: Option[Int], include: Boolean): String = {
    val expr1 = padRight match {
      case Some(padByte) => s"$expr0.bytesStripRight($padByte)"
      case None => expr0
    }
    val expr2 = terminator match {
      case Some(term) => s"$expr1.bytesTerminate($term, $include)"
      case None => expr1
    }
    expr2
  }
  def handleAssignmentIterative(id: Identifier, expr: String): Unit = {
    // Need better design for this XXX
    val exprName = id match {
      case _: RawIdentifier => translator.doName(Identifier.ITERATOR2)
      case _ => translator.doName(Identifier.ITERATOR)
    }
    out.puts(s"let $exprName = $expr")
    out.puts(s"${privateMemberName(id)}.add($exprName)")
  }
  override def handleAssignmentRepeatEos(id: Identifier, expr: String): Unit = {
    handleAssignmentIterative(id, expr)
  }
  override def handleAssignmentRepeatExpr(id: Identifier, expr: String): Unit = {
    handleAssignmentIterative(id, expr)
  }
  override def handleAssignmentRepeatUntil(id: Identifier, expr: String, isRaw: Boolean): Unit = {
    handleAssignmentIterative(id, expr)
  }
  override def handleAssignmentSimple(id: Identifier, expr: String): Unit = {
    // Need better design for this XXX
    val exprName = idToStr(id) + "Expr"
    out.puts(s"let $exprName = $expr")
    out.puts(s"${privateMemberName(id)} = $exprName")
  }
  override def handleAssignmentTempVar(dataType: DataType, id: String, expr: String): Unit = {}
  override def parseExpr(dataType: DataType, assignType: DataType, io: String, defEndian: Option[FixedEndian]): String = {
    val expr = dataType match {
      case t: ReadableType =>
        s"$io.read${Utils.capitalize(t.apiCall(defEndian))}()"
      case blt: BytesLimitType =>
        s"$io.readBytes(int(${expression(blt.size)}))"
      case _: BytesEosType =>
        s"$io.readBytesFull()"
      case BytesTerminatedType(terminator, include, consume, eosError, _) =>
        s"$io.readBytesTerm($terminator, $include, $consume, $eosError)"
      case BitsType1(bitEndian) =>
        s"$io.readBitsInt${camelCase(bitEndian.toSuffix, true)}(1) != 0"
      case BitsType(width: Int, bitEndian) =>
        s"$io.readBitsInt${camelCase(bitEndian.toSuffix, true)}($width)"
      case t: UserType =>
        val addArgs = {
          val parent = t.forcedParent match {
            case Some(USER_TYPE_NO_PARENT) => "nil"
            case Some(fp) => translator.translate(fp)
            case None => "this"
          }
          s", this.root, $parent"
        }
        val addParams = Utils.join(t.args.map((a) => translator.translate(a)), ", ", ", ", "")
        val concreteName = namespaced(t.classSpec match {
          case Some(cs) => cs.name
          case None => t.name
        })
        s"${concreteName}.read($io$addArgs$addParams)"
    }

    if (assignType != dataType) {
      s"${ksToNim(assignType)}($expr)"
    } else {
      expr
    }
  }
  override def userTypeDebugRead(id: String, dataType: DataType, assignType: DataType): Unit = {} // TODO

  // Members declared in io.kaitai.struct.languages.components.SwitchOps
  override def switchCasesUsingIf[T](
    id: Identifier,
    on: Ast.expr,
    onType: DataType,
    cases: Map[Ast.expr, T],
    normalCaseProc: (T) => Unit,
    elseCaseProc: (T) => Unit
  ): Unit = {
    switchIfStart(id, on, onType)

    // Pass 1: only normal case clauses
    var first = true

    cases.foreach { case (condition, result) =>
      condition match {
        case SwitchType.ELSE_CONST =>
          // skip for now
        case _ =>
          if (first) {
            switchIfCaseFirstStart(condition)
            first = false
          } else {
            switchIfCaseStart(condition)
          }
          normalCaseProc(result)
          switchIfCaseEnd()
      }
    }

    // Pass 2: else clause, if it is there
    cases.get(SwitchType.ELSE_CONST).foreach { (result) =>
      if (cases.size == 1) {
        elseCaseProc(result)
      } else {
        switchIfElseStart()
        elseCaseProc(result)
        switchIfElseEnd()
      }
    }

    switchIfEnd()
  }

  override def switchCaseEnd(): Unit = universalFooter
  override def switchCaseStart(condition: Ast.expr): Unit = {}
  override def switchElseStart(): Unit = {}
  override def switchEnd(): Unit = {}
  override def switchStart(id: Identifier, on: Ast.expr): Unit = {}

  // Members declared in io.kaitai.struct.languages.components.SwitchIfOps
  override def switchRequiresIfs(onType: DataType): Boolean = true
  override def switchIfStart(id: Identifier, on: Ast.expr, onType: DataType): Unit = {
    out.puts("block:")
    out.inc
    out.puts(s"let on = ${expression(on)}")
  }
  override def switchIfCaseFirstStart(condition: Ast.expr): Unit = {
    out.puts(s"if on == ${expression(condition)}:")
    out.inc
  }
  override def switchIfCaseStart(condition: Ast.expr): Unit = {
    out.puts(s"elif on == ${expression(condition)}:")
    out.inc
  }
  override def switchIfCaseEnd(): Unit = out.dec
  override def switchIfElseStart(): Unit = {
    out.puts("else:")
    out.inc
  }
  override def switchIfEnd(): Unit = out.dec

  // Members declared in io.kaitai.struct.languages.components.UniversalDoc
  override def universalDoc(doc: DocSpec): Unit = {
    out.puts
    out.puts( "##[")
    doc.summary.foreach(summary => out.puts(summary))
    doc.ref.foreach {
      case TextRef(text) =>
        out.puts("@see \"" + text + "\"")
      case ref: UrlRef =>
        out.puts(s"@see ${ref.toAhref}")
    }
    out.puts( "]##")
  }

  def instanceFlagIdentifier(id: InstanceIdentifier) = s"${idToStr(id)}Flag"
}

object NimCompiler extends LanguageCompilerStatic
  with StreamStructNames
  with UpperCamelCaseClasses
  with ExceptionNames {
  override def getCompiler(
    tp: ClassTypeProvider,
    config: RuntimeConfig
  ): LanguageCompiler = new NimCompiler(tp, config)

  // Members declared in io.kaitai.struct.languages.components.StreamStructNames
  override def kstreamName: String = "KaitaiStream"
  override def kstructName: String = "KaitaiStruct"
  def ksErrorName(err: KSError): String = "KaitaiError" // TODO: maybe add more debugging info

  def camelCase(s: String, upper: Boolean): String = {
    if (upper) {
      s.split("_").map(Utils.capitalize).mkString
    } else {
      if (s.startsWith("_")) {
        camelCase(s.substring(1), false)
      } else {
        val firstWord :: restWords = s.split("_").toList
        (firstWord :: restWords.map(Utils.capitalize)).mkString
      }
    }
  }

  def namespaced(names: List[String]): String = names.map(n => camelCase(n, true)).mkString("_")

  def ksToNim(attrType: DataType): String = {
    attrType match {
      case Int1Type(false) => "uint8"
      case IntMultiType(false, Width2, _) => "uint16"
      case IntMultiType(false, Width4, _) => "uint32"
      case IntMultiType(false, Width8, _) => "uint64"

      case Int1Type(true) => "int8"
      case IntMultiType(true, Width2, _) => "int16"
      case IntMultiType(true, Width4, _) => "int32"
      case IntMultiType(true, Width8, _) => "int64"

      case FloatMultiType(Width4, _) => "float32"
      case FloatMultiType(Width8, _) => "float64"

      case BitsType(_, _) => "uint64"

      case _: BooleanType => "bool"
      case CalcIntType => "int"
      case CalcFloatType => "float64"

      case _: StrType => "string"
      case _: BytesType => "seq[byte]"

      case KaitaiStructType | CalcKaitaiStructType => "KaitaiStruct"
      case KaitaiStreamType | OwnedKaitaiStreamType => "KaitaiStream"

      case t: UserType => namespaced(t.classSpec match {
        case Some(cs) => cs.name
        case None => t.name
      })

      case t: EnumType => namespaced(t.enumSpec.get.name)
      case at: ArrayType => s"seq[${ksToNim(at.elType)}]"
      case st: SwitchType => ksToNim(st.combinedType)

      case AnyType => "KaitaiStruct"
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy