From 5837c6fd7df90cbe9f4b61c7ebc125bafbdce269 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Wed, 25 Sep 2019 01:17:52 +0200 Subject: [PATCH] Add custom segment layout support --- docs/api/custom-platform.md | 7 + src/main/scala/millfork/Platform.scala | 20 ++ .../millfork/output/AbstractAssembler.scala | 292 ++++++++++-------- .../output/AbstractInliningCalculator.scala | 2 + .../scala/millfork/output/Deduplicate.scala | 4 +- .../scala/millfork/test/emu/EmuPlatform.scala | 1 + 6 files changed, 196 insertions(+), 130 deletions(-) diff --git a/docs/api/custom-platform.md b/docs/api/custom-platform.md index 3da11eba..4cb8bdc1 100644 --- a/docs/api/custom-platform.md +++ b/docs/api/custom-platform.md @@ -152,6 +152,13 @@ For better debugging on NES, RAM segments should use bank number `$ff`. * `segment_NAME_fill` – the byte value used to fill gaps and other unused space in the bank. Default: `0`. +* `segment_NAME_layout` – a comma-separated list of object names that defines in what order the objects are laid out in the segment. +One item has to be `*`, it means "all the other objects". +For example, `a,b,*,c,d` means that the output will contain `a` first, then `b`, then everything else except for `c` and `d`, +then `c` and finally `d`. +If an object from that list does not exist, it is ignored. +Default: `main,*` + #### `[output]` section * `style` – how multi-segment programs should be output: diff --git a/src/main/scala/millfork/Platform.scala b/src/main/scala/millfork/Platform.scala index d6019ea1..f85170e3 100644 --- a/src/main/scala/millfork/Platform.scala +++ b/src/main/scala/millfork/Platform.scala @@ -34,6 +34,7 @@ class Platform( val generateBbcMicroInfFile: Boolean, val generateGameBoyChecksums: Boolean, val bankNumbers: Map[String, Int], + val bankLayouts: Map[String, Seq[String]], val bankFill: Map[String, Int], val defaultCodeBank: String, val ramInitialValuesBank: Option[String], @@ -174,6 +175,24 @@ object Platform { case "" => 0 case x => parseNumber(x) })).toMap + // needed for ZX81 + val bankLayouts = banks.map(b => b -> { + val layout = as.get(classOf[String], s"segment_${b}_layout", "main,*").split(',').map(_.trim).toSeq + if (layout.isEmpty) { + log.error(s"Layout for segment $b shouldn't be empty") + } else { + if (!layout.contains("*")) { + log.error(s"Layout for segment $b should contain *") + } + if (layout.toSet.size != layout.size) { + log.error(s"Layout for segment $b should contains duplicates") + } + if (ramInitialValuesBank.contains(b) && layout.last != "*") { + log.warn(s"Layout for the ram_init_segment $b does not end with *") + } + } + layout + }).toMap // TODO: validate stuff banks.foreach(b => { @@ -281,6 +300,7 @@ object Platform { generateBbcMicroInfFile, generateGameBoyChecksums, bankNumbers, + bankLayouts, bankFills, defaultCodeBank, ramInitialValuesBank, diff --git a/src/main/scala/millfork/output/AbstractAssembler.scala b/src/main/scala/millfork/output/AbstractAssembler.scala index 1f4532db..b2f214eb 100644 --- a/src/main/scala/millfork/output/AbstractAssembler.scala +++ b/src/main/scala/millfork/output/AbstractAssembler.scala @@ -4,7 +4,7 @@ import millfork.assembly._ import millfork.compiler.{AbstractCompiler, CompilationContext} import millfork.env._ import millfork.error.Logger -import millfork.node.{CallGraph, Expression, LiteralExpression, NiceFunctionProperty, Program} +import millfork.node.{CallGraph, Expression, FunctionCallExpression, LiteralExpression, NiceFunctionProperty, Program, SumExpression} import millfork._ import millfork.assembly.z80.ZLine @@ -211,6 +211,7 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program val inliningResult = inliningCalculator.calculate( program, + platform.bankLayouts, options.flags(CompilationFlag.InlineFunctions) || options.flags(CompilationFlag.OptimizeForSonicSpeed), if (options.flags(CompilationFlag.OptimizeForSonicSpeed)) 4.0 else if (options.flags(CompilationFlag.OptimizeForSpeed)) 1.3 @@ -310,7 +311,9 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program bank0.readable(index) = true index += 1 } - case None => log.error(s"Non-constant contents of array `$name`", item.position) + case None => + env.debugConstness(item) + log.error(s"Non-constant contents of array `$name`: " + item, item.position) } } printArrayToAssemblyOutput(assembly, name, elementType, items) @@ -338,148 +341,179 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program val codeAllocators = platform.codeAllocators.mapValues(new VariableAllocator(Nil, _)).view.force var justAfterCode = platform.codeAllocators.mapValues(a => a.startAt).view.force - val sortedCompilerFunctions = compiledFunctions.toList.sortBy { case (name, cf) => if (name == "main") 0 -> "" else cf.orderKey } - sortedCompilerFunctions.filterNot(o => unusedRuntimeObjects(o._1)).foreach { - case (_, NormalCompiledFunction(_, _, true, _)) => - // already done before - case (name, NormalCompiledFunction(bank, functionCode, false, alignment)) => - val size = functionCode.map(_.sizeInBytes).sum - val bank0 = mem.banks(bank) - val index = codeAllocators(bank).allocateBytes(bank0, options, size, initialized = true, writeable = false, location = AllocationLocation.High, alignment = alignment) - labelMap(name) = bank0.index -> index - justAfterCode += bank -> outputFunction(bank, functionCode, index, assembly, options) - case _ => - } - sortedCompilerFunctions.foreach { - case (name, RedirectedFunction(_, target, offset)) => - val tuple = labelMap(target) - labelMap(name) = tuple._1 -> (tuple._2 + offset) - case _ => - } - // force early allocation of text literals: - env.allPreallocatables.filterNot(o => unusedRuntimeObjects(o.name)).foreach{ - case thing@InitializedArray(_, _, items, _, _, _, _, _) => - items.foreach(env.eval(_)) - case InitializedMemoryVariable(_, _, _, value, _, _, _) => - env.eval(value) - case _ => - } - - if (options.flag(CompilationFlag.LUnixRelocatableCode)) { - env.allThings.things.foreach { - case (_, m@UninitializedMemoryVariable(name, typ, _, _, _, _)) if name.endsWith(".addr") || env.maybeGet[Thing](name + ".array").isDefined => - val isUsed = compiledFunctions.values.exists{ - case NormalCompiledFunction(_, functionCode, _, _) => functionCode.exists(_.parameter.isRelatedTo(m)) - case _ => false - } -// println(m.name -> isUsed) - if (isUsed) { - val bank = m.bank(options) - if (bank != "default") ??? - val bank0 = mem.banks(bank) - var index = codeAllocators(bank).allocateBytes(bank0, options, typ.size + 1, initialized = true, writeable = false, location = AllocationLocation.High, alignment = m.alignment) - labelMap(name) = bank0.index -> (index + 1) - val altName = m.name.stripPrefix(env.prefix) + "`" - val thing = if (name.endsWith(".addr")) env.get[ThingInMemory](name.stripSuffix(".addr")) else env.get[ThingInMemory](name + ".array") - env.things += altName -> ConstantThing(altName, NumericConstant(index, 2), env.get[Type]("pointer")) - assembly.append("* = $" + index.toHexString) - assembly.append(" " + bytePseudoopcode + " $2c") - assembly.append(name + ":") - val c = thing.toAddress - writeByte(bank, index, 0x2c.toByte) // BIT abs - index += 1 - if (platform.isBigEndian) { - throw new IllegalStateException("LUnix cannot run on big-endian architectures") - } - for (i <- 0 until typ.size) { - writeByte(bank, index, subbyte(c, i, typ.size)) - assembly.append(" " + bytePseudoopcode + " " + subbyte(c, i, typ.size).quickSimplify) - index += 1 - } - initializedVariablesSize += typ.size - justAfterCode += bank -> index - } - case _ => () + def getLayoutStage(name: String, segment: String): Int = { + var result = platform.bankLayouts(segment).indexOf(name) + if (result < 0) { + result = platform.bankLayouts(segment).indexOf("*") } - val index = codeAllocators("default").allocateBytes(mem.banks("default"), options, 1, initialized = true, writeable = false, location = AllocationLocation.High, alignment = NoAlignment) - writeByte("default", index, 2.toByte) // BIT abs - assembly.append("* = $" + index.toHexString) - assembly.append(" " + bytePseudoopcode + " 2 ;; end of LUnix relocatable segment") - justAfterCode += "default" -> (index + 1) + // log.trace(s"Emit stage for ${name} is $result") + result } - env.getAllFixedAddressObjects.foreach { - case (bank, addr, size) => - val bank0 = mem.banks(bank) - for(i <- 0 until size) bank0.occupied(addr + i) = true - variableAllocators(bank).notifyAboutHole(bank0, addr, size) + + @inline + def getLayoutStageThing(th: ThingInMemory): Int = getLayoutStage(th.name, th.bank(options)) + + @inline + def getLayoutStageNcf(name: String, th: NormalCompiledFunction[_]): Int = getLayoutStage(name, th.segment) + + val layoutStageCount = platform.bankLayouts.values.map(_.length).max + val defaultStage = platform.bankLayouts("default").indexOf("*") + if (defaultStage < 0) { + log.fatal("The layout for the default segment lacks *") } var rwDataStart = Int.MaxValue var rwDataEnd = 0 - for(readOnlyPass <- Seq(true, false)) { - if (!readOnlyPass) { - if (options.platform.ramInitialValuesBank.isDefined) { - codeAllocators("default").notifyAboutEndOfCode(codeAllocators("default").heapStart) + + val sortedCompilerFunctions = compiledFunctions.toList.sortBy { case (_, cf) => cf.orderKey } + for (layoutStage <- 0 until layoutStageCount) { + sortedCompilerFunctions.filterNot(o => unusedRuntimeObjects(o._1)).foreach { + case (_, NormalCompiledFunction(_, _, true, _)) => + // already done before + case (name, th@NormalCompiledFunction(bank, functionCode, false, alignment)) if layoutStage == getLayoutStageNcf(name, th) => + val size = functionCode.map(_.sizeInBytes).sum + val bank0 = mem.banks(bank) + val index = codeAllocators(bank).allocateBytes(bank0, options, size, initialized = true, writeable = false, location = AllocationLocation.High, alignment = alignment) + labelMap(name) = bank0.index -> index + justAfterCode += bank -> outputFunction(bank, functionCode, index, assembly, options) + case _ => + } + sortedCompilerFunctions.foreach { + case (name, RedirectedFunction(segment, target, offset)) if (layoutStage == getLayoutStage(target, segment)) => + val tuple = labelMap(target) + labelMap(name) = tuple._1 -> (tuple._2 + offset) + case _ => + } + if (layoutStage == 0) { + // force early allocation of text literals: + env.allPreallocatables.filterNot(o => unusedRuntimeObjects(o.name)).foreach { + case thing@InitializedArray(_, _, items, _, _, _, _, _) => + items.foreach(env.eval(_)) + case InitializedMemoryVariable(_, _, _, value, _, _, _) => + env.eval(value) + case _ => } } - env.allPreallocatables.filterNot(o => unusedRuntimeObjects(o.name)).foreach { - case thing@InitializedArray(name, None, items, _, _, elementType, readOnly, alignment) if readOnly == readOnlyPass => - val bank = thing.bank(options) - if (options.platform.ramInitialValuesBank.isDefined && !readOnly && bank != "default") { - log.error(s"Preinitialized writable array `$name` should be defined in the `default` bank") - } - val bank0 = mem.banks(bank) - var index = codeAllocators(bank).allocateBytes(bank0, options, thing.sizeInBytes, initialized = true, writeable = true, location = AllocationLocation.High, alignment = alignment) - labelMap(name) = bank0.index -> index - if (!readOnlyPass) { - rwDataStart = rwDataStart.min(index) - rwDataEnd = rwDataEnd.max(index + thing.sizeInBytes) - } - assembly.append("* = $" + index.toHexString) - assembly.append(name + ":") - for (item <- items) { - env.eval(item) match { - case Some(c) => - for (i <- 0 until elementType.size) { - writeByte(bank, index, subbyte(c, i, elementType.size)) - index += 1 - } - case None => log.error(s"Non-constant contents of array `$name`", item.position) + + if (layoutStage == defaultStage && options.flag(CompilationFlag.LUnixRelocatableCode)) { + env.allThings.things.foreach { + case (_, m@UninitializedMemoryVariable(name, typ, _, _, _, _)) if name.endsWith(".addr") || env.maybeGet[Thing](name + ".array").isDefined => + val isUsed = compiledFunctions.values.exists { + case NormalCompiledFunction(_, functionCode, _, _) => functionCode.exists(_.parameter.isRelatedTo(m)) + case _ => false } - } - printArrayToAssemblyOutput(assembly, name, elementType, items) - initializedVariablesSize += items.length - justAfterCode += bank -> index - case m@InitializedMemoryVariable(name, None, typ, value, _, alignment, _) if !readOnlyPass=> - val bank = m.bank(options) - if (options.platform.ramInitialValuesBank.isDefined && bank != "default") { - log.error(s"Preinitialized variable `$name` should be defined in the `default` bank") - } - val bank0 = mem.banks(bank) - var index = codeAllocators(bank).allocateBytes(bank0, options, typ.size, initialized = true, writeable = true, location = AllocationLocation.High, alignment = alignment) - labelMap(name) = bank0.index -> index - if (!readOnlyPass) { - rwDataStart = rwDataStart.min(index) - rwDataEnd = rwDataEnd.max(index + typ.size) - } - val altName = m.name.stripPrefix(env.prefix) + "`" - env.things += altName -> ConstantThing(altName, NumericConstant(index, 2), env.get[Type]("pointer")) - assembly.append("* = $" + index.toHexString) - assembly.append(name + ":") - env.eval(value) match { - case Some(c) => + // println(m.name -> isUsed) + if (isUsed) { + val bank = m.bank(options) + if (bank != "default") ??? + val bank0 = mem.banks(bank) + var index = codeAllocators(bank).allocateBytes(bank0, options, typ.size + 1, initialized = true, writeable = false, location = AllocationLocation.High, alignment = m.alignment) + labelMap(name) = bank0.index -> (index + 1) + val altName = m.name.stripPrefix(env.prefix) + "`" + val thing = if (name.endsWith(".addr")) env.get[ThingInMemory](name.stripSuffix(".addr")) else env.get[ThingInMemory](name + ".array") + env.things += altName -> ConstantThing(altName, NumericConstant(index, 2), env.get[Type]("pointer")) + assembly.append("* = $" + index.toHexString) + assembly.append(" " + bytePseudoopcode + " $2c") + assembly.append(name + ":") + val c = thing.toAddress + writeByte(bank, index, 0x2c.toByte) // BIT abs + index += 1 + if (platform.isBigEndian) { + throw new IllegalStateException("LUnix cannot run on big-endian architectures") + } for (i <- 0 until typ.size) { writeByte(bank, index, subbyte(c, i, typ.size)) assembly.append(" " + bytePseudoopcode + " " + subbyte(c, i, typ.size).quickSimplify) index += 1 } - case None => - log.error(s"Non-constant initial value for variable `$name`") - index += typ.size + initializedVariablesSize += typ.size + justAfterCode += bank -> index + } + case _ => () + } + val index = codeAllocators("default").allocateBytes(mem.banks("default"), options, 1, initialized = true, writeable = false, location = AllocationLocation.High, alignment = NoAlignment) + writeByte("default", index, 2.toByte) // BIT abs + assembly.append("* = $" + index.toHexString) + assembly.append(" " + bytePseudoopcode + " 2 ;; end of LUnix relocatable segment") + justAfterCode += "default" -> (index + 1) + } + if (layoutStage == 0) { + env.getAllFixedAddressObjects.foreach { + case (bank, addr, size) => + val bank0 = mem.banks(bank) + for (i <- 0 until size) bank0.occupied(addr + i) = true + variableAllocators(bank).notifyAboutHole(bank0, addr, size) + } + } + for (readOnlyPass <- Seq(true, false)) { + if (layoutStage == defaultStage && !readOnlyPass) { + if (options.platform.ramInitialValuesBank.isDefined) { + codeAllocators("default").notifyAboutEndOfCode(codeAllocators("default").heapStart) } - initializedVariablesSize += typ.size - justAfterCode += bank -> index - case _ => + } + env.allPreallocatables.filterNot(o => unusedRuntimeObjects(o.name)).foreach { + case thing@InitializedArray(name, None, items, _, _, elementType, readOnly, alignment) if readOnly == readOnlyPass && layoutStage == getLayoutStageThing(thing) => + val bank = thing.bank(options) + if (options.platform.ramInitialValuesBank.isDefined && !readOnly && bank != "default") { + log.error(s"Preinitialized writable array `$name` should be defined in the `default` bank") + } + val bank0 = mem.banks(bank) + var index = codeAllocators(bank).allocateBytes(bank0, options, thing.sizeInBytes, initialized = true, writeable = true, location = AllocationLocation.High, alignment = alignment) + labelMap(name) = bank0.index -> index + if (!readOnlyPass) { + rwDataStart = rwDataStart.min(index) + rwDataEnd = rwDataEnd.max(index + thing.sizeInBytes) + } + assembly.append("* = $" + index.toHexString) + assembly.append(name + ":") + for (item <- items) { + val w = env + env.eval(item) match { + case Some(c) => + for (i <- 0 until elementType.size) { + writeByte(bank, index, subbyte(c, i, elementType.size)) + index += 1 + } + case None => + env.debugConstness(item) + log.error(s"Non-constant contents of array `$name`:" + item, item.position) + } + } + printArrayToAssemblyOutput(assembly, name, elementType, items) + initializedVariablesSize += items.length + justAfterCode += bank -> index + case m@InitializedMemoryVariable(name, None, typ, value, _, alignment, _) if !readOnlyPass && layoutStage == getLayoutStageThing(m) => + val bank = m.bank(options) + if (options.platform.ramInitialValuesBank.isDefined && bank != "default") { + log.error(s"Preinitialized variable `$name` should be defined in the `default` bank") + } + val bank0 = mem.banks(bank) + var index = codeAllocators(bank).allocateBytes(bank0, options, typ.size, initialized = true, writeable = true, location = AllocationLocation.High, alignment = alignment) + labelMap(name) = bank0.index -> index + if (!readOnlyPass) { + rwDataStart = rwDataStart.min(index) + rwDataEnd = rwDataEnd.max(index + typ.size) + } + val altName = m.name.stripPrefix(env.prefix) + "`" + env.things += altName -> ConstantThing(altName, NumericConstant(index, 2), env.get[Type]("pointer")) + assembly.append("* = $" + index.toHexString) + assembly.append(name + ":") + env.eval(value) match { + case Some(c) => + for (i <- 0 until typ.size) { + writeByte(bank, index, subbyte(c, i, typ.size)) + assembly.append(" " + bytePseudoopcode + " " + subbyte(c, i, typ.size).quickSimplify) + index += 1 + } + case None => + env.debugConstness(value) + log.error(s"Non-constant initial value for variable `$name`") + index += typ.size + } + initializedVariablesSize += typ.size + justAfterCode += bank -> index + case _ => + } } } if (rwDataEnd == 0 && rwDataStart == Int.MaxValue) { diff --git a/src/main/scala/millfork/output/AbstractInliningCalculator.scala b/src/main/scala/millfork/output/AbstractInliningCalculator.scala index 61d0e0c8..05648dd7 100644 --- a/src/main/scala/millfork/output/AbstractInliningCalculator.scala +++ b/src/main/scala/millfork/output/AbstractInliningCalculator.scala @@ -25,6 +25,7 @@ abstract class AbstractInliningCalculator[T <: AbstractCode] { private val sizes = Seq(64, 64, 8, 6, 5, 5, 4) def calculate(program: Program, + bankLayouts: Map[String, Seq[String]], inlineByDefault: Boolean, aggressivenessForNormal: Double, aggressivenessForRecommended: Double): InliningResult = { @@ -48,6 +49,7 @@ abstract class AbstractInliningCalculator[T <: AbstractCode] { || f.interrupt || f.reentrant || f.name == "main" + || bankLayouts.values.exists(_.contains(f.name)) || containsReturnDispatch(f.statements.getOrElse(Nil))) badFunctions += f.name case _ => } diff --git a/src/main/scala/millfork/output/Deduplicate.scala b/src/main/scala/millfork/output/Deduplicate.scala index 6bd59a51..f799ce99 100644 --- a/src/main/scala/millfork/output/Deduplicate.scala +++ b/src/main/scala/millfork/output/Deduplicate.scala @@ -174,7 +174,7 @@ abstract class Deduplicate[T <: AbstractCode](env: Environment, options: Compila val representative = if (set("main")) "main" else set.head options.log.debug(s"Functions [${set.mkString(",")}] are identical") for (function <- set) { - if (function != representative) { + if (function != representative && !options.platform.bankLayouts(segmentName).contains(function)) { result += function -> RedirectedFunction(segmentName, representative, 0) } else { segContents(function) match { @@ -216,6 +216,7 @@ abstract class Deduplicate[T <: AbstractCode](env: Environment, options: Compila else getJump(code.last) .filter(segContents.contains) .filter(_ != name) + .filter(n => !options.platform.bankLayouts(segmentName).contains(n)) .filter(_ != "main") .flatMap(to => follow(segContents, to)) .map(name -> _) @@ -250,6 +251,7 @@ abstract class Deduplicate[T <: AbstractCode](env: Environment, options: Compila else getJump(code.last) .filter(segContents.contains) .filter(_ != name) + .filter(n => !options.platform.bankLayouts(segmentName).contains(n)) .filter(_ != "main") .map(name -> _) case _ => None diff --git a/src/test/scala/millfork/test/emu/EmuPlatform.scala b/src/test/scala/millfork/test/emu/EmuPlatform.scala index fe23e1d5..34fdaf6b 100644 --- a/src/test/scala/millfork/test/emu/EmuPlatform.scala +++ b/src/test/scala/millfork/test/emu/EmuPlatform.scala @@ -36,6 +36,7 @@ object EmuPlatform { false, false, Map("default" -> 0, "second" -> 2, "third" -> 3), + Map("default" -> Seq("main", "*"), "second" -> Seq("main", "*"), "third" -> Seq("main", "*")), Map("default" -> 0, "second" -> 0, "third" -> 0), "default", None,