1
0
mirror of https://github.com/KarolS/millfork.git synced 2024-05-31 18:41:30 +00:00
millfork/src/main/scala/millfork/Platform.scala

492 lines
20 KiB
Scala

package millfork
import java.io.{File, StringReader}
import java.nio.charset.StandardCharsets
import java.nio.file.{Files, Paths}
import java.util.Locale
import millfork.error.Logger
import millfork.output._
import millfork.parser.{TextCodec, TextCodecRepository, TextCodecWithFlags}
import org.apache.commons.configuration2.INIConfiguration
/**
* @author Karol Stasiak
*/
object OutputStyle extends Enumeration {
val Single, PerBank, LUnix = Value
}
class Platform(
val cpu: Cpu.Value,
val flagOverrides: Map[CompilationFlag.Value, Boolean],
val startingModules: List[String],
val defaultCodec: TextCodec,
val screenCodec: TextCodec,
val features: Map[String, Long],
val outputPackagers: Map[String, OutputPackager],
val defaultOutputPackager: OutputPackager,
val codeAllocators: Map[String, UpwardByteAllocator],
val variableAllocators: Map[String, VariableAllocator],
val zpRegisterSize: Int,
val freeZpBytes: List[Int],
val fileExtension: String,
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],
val outputLabelsFormat: DebugOutputFormat,
val outputStyle: OutputStyle.Value
) {
def hasZeroPage: Boolean = cpuFamily == CpuFamily.M6502
def cpuFamily: CpuFamily.Value = CpuFamily.forType(this.cpu)
def isBigEndian: Boolean = CpuFamily.isBigEndian(cpuFamily)
private def bankStart(bank: String): Int = codeAllocators(bank).startAt min variableAllocators(bank).startAt
private def bankEndBefore(bank: String): Int = codeAllocators(bank).endBefore max variableAllocators(bank).endBefore
def isUnsafeToJump(bank1: String, bank2: String): Boolean = {
if (bank1 == bank2) return false
val s1 = bankStart(bank1)
val s2 = bankStart(bank2)
val e1 = bankEndBefore(bank1)
val e2 = bankEndBefore(bank2)
e2 > s1 && s2 < e1
}
lazy val banksExplicitlyWrittenToOutput: Set[String] = bankNumbers.keySet.filter(b => defaultOutputPackager.writes(b)).toSet
def getMesenLabelCategory(bank: String, address: Int): Char = {
// see: https://www.mesen.ca/docs/debugging/debuggerintegration.html#mesen-label-files-mlb
val start = bankStart(bank)
val end = bankEndBefore(bank)
if (start > address || address >= end) 'G'
else if (banksExplicitlyWrittenToOutput(bank)) 'P'
else if (bank == "default") 'R'
else 'W' // TODO: distinguish between W and S
}
}
object Platform {
def lookupPlatformFile(includePath: List[String], platformName: String, textCodecRepository: TextCodecRepository)(implicit log: Logger): Platform = {
includePath.foreach { dir =>
val file = Paths.get(dir, platformName + ".ini").toFile
log.debug("Checking " + file)
if (file.exists()) {
return load(file, textCodecRepository)
}
val file2 = Paths.get(dir, "platform", platformName + ".ini").toFile
log.debug("Checking " + file2)
if (file2.exists()) {
return load(file2, textCodecRepository)
}
}
log.fatal(s"Platform definition `$platformName` not found", None)
}
def load(file: File, textCodecRepository: TextCodecRepository)(implicit log: Logger): Platform = {
val conf = new INIConfiguration()
val bytes = Files.readAllBytes(file.toPath)
conf.read(new StringReader(new String(bytes, StandardCharsets.UTF_8)))
val cs = conf.getSection("compilation")
val cpu = Cpu.fromString(cs.get(classOf[String], "arch", "strict"))
val value65816 = cs.get(classOf[String], "emit_65816", "")
val flagOverrides = (value65816.toLowerCase match {
case "" => Nil
case "false" | "none" | "no" | "off" | "0" =>
List(
CompilationFlag.EmitEmulation65816Opcodes -> false,
CompilationFlag.EmitNative65816Opcodes -> false,
CompilationFlag.ReturnWordsViaAccumulator -> false)
case "emulation" =>
List(
CompilationFlag.EmitEmulation65816Opcodes -> true,
CompilationFlag.EmitNative65816Opcodes -> false,
CompilationFlag.ReturnWordsViaAccumulator -> false)
case "native" =>
List(
CompilationFlag.EmitEmulation65816Opcodes -> true,
CompilationFlag.EmitNative65816Opcodes -> true)
case _ =>
log.error(s"Unsupported `emit_65816` value: $value65816")
Nil
}).toMap ++ CompilationFlag.fromString.flatMap { case (k, f) =>
val value = cs.get(classOf[String], k, "")
value.toLowerCase match {
case "" | null => None
case "false" | "off" | "no" | "0" => Some(f -> false)
case "true" | "on" | "yes" | "1" => Some(f -> true)
case _ =>
log.error(s"Unsupported `$k` value: $value")
None
}
}
val startingModules = cs.get(classOf[String], "modules", "").split("[, ]+").filter(_.nonEmpty).toList
val zpRegisterSize = cs.get(classOf[String], "zeropage_register", "").toLowerCase match {
case "" | null => if (CpuFamily.forType(cpu) == CpuFamily.M6502) 4 else 0
case "true" | "on" | "yes" => 4
case "false" | "off" | "no" | "0" => 0
case x => x.toInt
}
if (zpRegisterSize < 0 || zpRegisterSize > 128) {
log.error("Invalid zeropage register size: " + zpRegisterSize)
}
val codecName = cs.get(classOf[String], "encoding", "ascii")
val srcCodecName = cs.get(classOf[String], "screen_encoding", codecName)
val TextCodecWithFlags(codec, czt, clp, _) = textCodecRepository.forName(codecName, None, log)
if (czt) {
log.error("Default encoding cannot be zero-terminated")
}
if (clp) {
log.error("Default encoding cannot be length-prefixed")
}
if (codec.stringTerminator.length != 1) {
log.warn("Default encoding should be byte-based")
}
val TextCodecWithFlags(srcCodec, szt, slp, _) = textCodecRepository.forName(srcCodecName, None, log)
if (szt) {
log.error("Default screen encoding cannot be zero-terminated")
}
if (slp) {
log.error("Default screen encoding cannot be length-prefixed")
}
val as = conf.getSection("allocation")
val banks = as.get(classOf[String], "segments", "default").split("[, ]+").filter(_.nonEmpty).toList
if (!banks.contains("default")) {
log.error("A segment named `default` is required")
}
if (banks.toSet.size != banks.length) {
log.error("Duplicate segment name")
}
val BankRegex = """\A[A-Za-z0-9_]+\z""".r
banks.foreach {
case BankRegex(_*) => // ok
case b => log.error(s"Invalid segment name: `$b`")
}
val bankStarts = banks.map(b => b -> (as.get(classOf[String], s"segment_${b}_start") match {
case "" | null => log.error(s"Undefined segment_${b}_start"); 0
case x => parseNumber(x)
})).toMap
val bankDataStarts = banks.map(b => b -> (as.get(classOf[String], s"segment_${b}_datastart", "after_code") match {
case "" | "after_code" => None
case x => Some(parseNumber(x))
})).toMap
val bankEnds = banks.map(b => b -> (as.get(classOf[String], s"segment_${b}_end") match {
case "" | null => log.error(s"Undefined segment_${b}_end"); 0xffff
case x => parseNumber(x)
})).toMap
val bankCodeEnds = banks.map(b => b -> (as.get(classOf[String], s"segment_${b}_codeend", "") match {
case "" => bankEnds(b)
case x => parseNumber(x)
})).toMap
val defaultCodeBank = as.get(classOf[String], "default_code_segment") match {
case "" | null => "default"
case x => x
}
val ramInitialValuesBank = as.get(classOf[String], "ram_init_segment") match {
case "" | null => None
case "default" =>
log.error("Cannot use default as ram_init_segment")
None
case x if banks.contains(x) => Some(x)
case x =>
log.error("Invalid ram_init_segment: " + x)
None
}
// used by 65816 and in NES debugging:
val bankNumbers = banks.map(b => b -> (as.get(classOf[String], s"segment_${b}_bank", "00") match {
case "" => 0
case x => parseNumber(x)
})).toMap
val bankFills = banks.map(b => b -> (as.get(classOf[String], s"segment_${b}_fill", "00") match {
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 => {
if (bankNumbers(b) < 0 || bankNumbers(b) > 255) log.error(s"Segment $b has invalid bank")
if (bankStarts(b) >= bankCodeEnds(b)) log.error(s"Segment $b has invalid range")
if (bankCodeEnds(b) > bankEnds(b)) log.error(s"Segment $b has invalid range")
if (bankStarts(b) >= bankEnds(b)) log.error(s"Segment $b has invalid range")
bankDataStarts(b).foreach(dataStarts => if (dataStarts >= bankEnds(b)) log.error(s"Segment $b has invalid range"))
})
val freePointers: Option[List[Int]] = as.get(classOf[String], "zp_pointers", "") match {
case "all" => Some(List.tabulate(128)(_ * 2))
case "" => None
case xs => Some(xs.split("[, ]+").flatMap(s => parseNumberOrRange(s, 2)).toList)
}
val freeExplicitBytes: Option[List[Int]] = as.get(classOf[String], "zp_bytes", "") match {
case "all" => Some(List.tabulate(256)(identity))
case "" => None
case xs => Some(xs.split("[, ]+").flatMap(s => parseNumberOrRange(s, 1)).toList)
}
val freeZpBytes: List[Int] = (freePointers, freeExplicitBytes) match {
case (Some(l), None) => l.flatMap(i => List(i, i+1))
case (None, Some(l)) => l
case (None, None) => List.tabulate(256)(identity)
case (Some(_), Some(l)) =>
log.error(s"Cannot define both zp_pointers and zp_bytes")
l
}
val codeAllocators = banks.map(b => b -> new UpwardByteAllocator(bankStarts(b), bankCodeEnds(b) + 1))
val variableAllocators = banks.map(b => b -> new VariableAllocator(
if (b == "default" && CpuFamily.forType(cpu) == CpuFamily.M6502) freeZpBytes else Nil, bankDataStarts(b) match {
case None => new AfterCodeByteAllocator(bankStarts(b), bankEnds(b) + 1)
case Some(start) => new UpwardByteAllocator(start, bankEnds(b) + 1)
}))
val os = conf.getSection("output")
def parseOutputPackager(s: String): OutputPackager = SequenceOutput(s.split("[, \n\t\r]+").filter(_.nonEmpty).map {
case "startaddr" => StartAddressOutput(0)
case "startaddr_be" => StartAddressOutputBe(0)
case "startpage" => StartPageOutput
case "endaddr" => EndAddressOutput(0)
case "endaddr_be" => EndAddressOutputBe(0)
case l if l.startsWith("\"") && l.endsWith("\"") => StringOutput(l.substring(1, l.length - 1))
case l if l.startsWith("programname-") => ProgramNameOutput(parseNumber(l.stripPrefix("programname-")))
case l if l.startsWith("startaddr+") => StartAddressOutput(parseNumber(l.stripPrefix("startaddr+")))
case l if l.startsWith("startaddr-") => StartAddressOutput(-parseNumber(l.stripPrefix("startaddr-")))
case l if l.startsWith("startaddr_be+") => StartAddressOutputBe(parseNumber(l.stripPrefix("startaddr_be+")))
case l if l.startsWith("startaddr_be-") => StartAddressOutputBe(-parseNumber(l.stripPrefix("startaddr_be-")))
case l if l.startsWith("endaddr+") => EndAddressOutput(parseNumber(l.stripPrefix("endaddr+")))
case l if l.startsWith("endaddr-") => EndAddressOutput(-parseNumber(l.stripPrefix("endaddr-")))
case l if l.startsWith("endaddr_be+") => EndAddressOutputBe(parseNumber(l.stripPrefix("endaddr_be+")))
case l if l.startsWith("endaddr_be-") => EndAddressOutputBe(-parseNumber(l.stripPrefix("endaddr_be-")))
case "pagecount" => PageCountOutput
case "allocated" => AllocatedDataOutput
case "length" => AllocatedDataLength(0)
case "length_be" => AllocatedDataLengthBe(0)
case l if l.startsWith("length+") => AllocatedDataLength(parseNumber(l.stripPrefix("length+")))
case l if l.startsWith("length_be+") => AllocatedDataLengthBe(parseNumber(l.stripPrefix("length_be+")))
case l if l.startsWith("length-") => AllocatedDataLength(-parseNumber(l.stripPrefix("length-")))
case l if l.startsWith("length_be-") => AllocatedDataLengthBe(-parseNumber(l.stripPrefix("length_be-")))
case "length_be" => AllocatedDataLengthBe(0)
case "d88" => D88Output
case "tap" => new TapOutput("main")
case l if l.startsWith("tap:") => new TapOutput(l.stripPrefix("tap:").trim)
case "trscmd" => new TrsCmdOutput("main")
case l if l.startsWith("trscmd:") => new TrsCmdOutput(l.stripPrefix("trscmd:").trim)
case n => n.split(":").filter(_.nonEmpty) match {
case Array(b, s, e) => BankFragmentOutput(b, parseNumber(s), parseNumber(e))
case Array(s, e) =>
s match {
case "addr" =>
val (symbol, bonus) = parseSymbolAndBonus(e)
SymbolAddressOutput(symbol, bonus)
case "addr_be" =>
val (symbol, bonus) = parseSymbolAndBonus(e)
SymbolAddressOutputBe(symbol, bonus)
case _ =>
CurrentBankFragmentOutput(parseNumber(s), parseNumber(e))
}
case Array(b) => try {
ConstOutput(parseNumber(b).toByte)
} catch {
case _:NumberFormatException => log.fatal(s"Invalid output format: `$b`")
}
case x => log.fatal(s"Invalid output format: `$x`")
}
}.toList)
val outputPackagers = banks.flatMap{ b =>
val f = os.get(classOf[String], "format_segment_" + b, null)
if (f eq null) {
None
} else {
Some(b -> parseOutputPackager(f))
}
}.toMap
val defaultOutputPackager = parseOutputPackager(os.get(classOf[String], "format", ""))
val fileExtension = os.get(classOf[String], "extension", ".bin")
val generateBbcMicroInfFile = os.get(classOf[Boolean], "bbc_inf", false)
val generateGameBoyChecksums = os.get(classOf[Boolean], "gb_checksum", false)
val outputStyle = os.get(classOf[String], "style", "single") match {
case "" | "single" => OutputStyle.Single
case "per_bank" | "per_segment" => OutputStyle.PerBank
case "lunix" => OutputStyle.LUnix
case x => log.fatal(s"Invalid output style: `$x`")
}
if (outputStyle != OutputStyle.PerBank && outputPackagers.nonEmpty) {
log.error("Different output formats per segment are allowed only if style=per_segment")
}
val debugOutputFormatName = os.get(classOf[String], "labels", "vice")
val debugOutputFormat = DebugOutputFormat.map.getOrElse(
debugOutputFormatName.toLowerCase(Locale.ROOT),
log.fatal(s"Invalid label file format: `$debugOutputFormatName`"))
val builtInFeatures = builtInCpuFeatures(cpu) ++ Map(
"ENCODING_NOLOWER" -> toLong(codec.supportsLowercase),
"ENCODING_SAME" -> toLong(codec.name == srcCodec.name),
"DECIMALS_SAME" -> toLong(codec.stringTerminator == srcCodec.stringTerminator && (0 to 9).forall{c =>
codec.encodeDigit(c) == srcCodec.encodeDigit(c)
}),
"ENCCONV_SUPPORTED" -> toLong((codec.name, srcCodec.name) match {
// TODO: don't rely on names!
case ("PETSCII", "CBM-Screen") |
("PETSCII-JP", "CBM-Screen-JP") |
("ATASCII", "ATASCII-Screen") =>
CpuFamily.forType(cpu) == CpuFamily.M6502
case ("Color-Computer", "Color-Computer-Screen") =>
CpuFamily.forType(cpu) == CpuFamily.M6809
case _ => codec.name == srcCodec.name
}),
"NULLCHAR_SAME" -> toLong(codec.stringTerminator == srcCodec.stringTerminator)
)
import scala.collection.JavaConverters._
val ds = conf.getSection("define")
val definedFeatures = ds.getKeys().asScala.toList.map { k =>
val value = ds.get(classOf[String], k).trim() match {
case "true" | "on" | "yes" => 1L
case "false" | "off" | "no" | "" => 0L
case x => x.toLong
}
k -> value
}.toMap
var actualStartingModules = startingModules
if (ramInitialValuesBank.isDefined) {
actualStartingModules = "init_rw_memory" :: actualStartingModules
}
new Platform(
cpu,
flagOverrides,
actualStartingModules,
codec,
srcCodec,
builtInFeatures ++ definedFeatures,
outputPackagers,
defaultOutputPackager,
codeAllocators.toMap,
variableAllocators.toMap,
zpRegisterSize,
freeZpBytes,
if (fileExtension == "" || fileExtension.startsWith(".")) fileExtension else "." + fileExtension,
generateBbcMicroInfFile,
generateGameBoyChecksums,
bankNumbers,
bankLayouts,
bankFills,
defaultCodeBank,
ramInitialValuesBank,
debugOutputFormat,
outputStyle)
}
@inline
private def toLong(b: Boolean): Long = if (b) 1L else 0L
def builtInCpuFeatures(cpu: Cpu.Value): Map[String, Long] = {
Map[String, Long](
"ARCH_6502" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M6502),
"CPU_6502" -> toLong(Set(Cpu.Mos, Cpu.StrictMos, Cpu.Ricoh, Cpu.StrictRicoh)(cpu)),
"CPU_65C02" -> toLong(cpu == Cpu.Cmos),
"CPU_65CE02" -> toLong(cpu == Cpu.CE02),
"CPU_65816" -> toLong(cpu == Cpu.Sixteen),
"CPU_HUC6280" -> toLong(cpu == Cpu.HuC6280),
"ARCH_I80" -> toLong(CpuFamily.forType(cpu) == CpuFamily.I80),
"CPU_Z80" -> toLong(cpu == Cpu.Z80),
"CPU_EZ80" -> toLong(cpu == Cpu.EZ80),
"CPU_8080" -> toLong(cpu == Cpu.Intel8080),
"CPU_8085" -> toLong(cpu == Cpu.Intel8085),
"CPU_GAMEBOY" -> toLong(cpu == Cpu.Sharp),
"ARCH_X86" -> toLong(CpuFamily.forType(cpu) == CpuFamily.I86),
"CPU_8086" -> toLong(cpu == Cpu.Intel8086),
"CPU_80186" -> toLong(cpu == Cpu.Intel80186),
"ARCH_6800" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M6800),
"ARCH_6809" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M6809),
"ARCH_ARM" -> toLong(CpuFamily.forType(cpu) == CpuFamily.ARM),
"ARCH_68K" -> toLong(CpuFamily.forType(cpu) == CpuFamily.M68K)
// TODO
)
}
def parseNumberOrRange(s:String, step: Int)(implicit log: Logger): Seq[Int] = {
if (s.contains("-")) {
val segments = s.split("-")
if (segments.length != 2) {
log.fatal(s"Invalid range: `$s`")
}
Range(parseNumber(segments(0).trim()), parseNumber(segments(1).trim()) + 1, step)
} else {
Seq(parseNumber(s.trim()))
}
}
def parseNumber(str: String): Int = {
val s = str.trim
if (s.startsWith("$")) {
Integer.parseInt(s.substring(1), 16)
} else if (s.startsWith("0x")) {
Integer.parseInt(s.substring(2), 16)
} else if (s.startsWith("0X")) {
Integer.parseInt(s.substring(2), 16)
} else if (s.startsWith("%")) {
Integer.parseInt(s.substring(1), 2)
} else if (s.startsWith("0b")) {
Integer.parseInt(s.substring(2), 2)
} else if (s.startsWith("0B")) {
Integer.parseInt(s.substring(2), 2)
} else if (s.startsWith("0o")) {
Integer.parseInt(s.substring(2), 8)
} else if (s.startsWith("0O")) {
Integer.parseInt(s.substring(2), 8)
} else if (s.startsWith("0q")) {
Integer.parseInt(s.substring(2), 4)
} else if (s.startsWith("0Q")) {
Integer.parseInt(s.substring(2), 4)
} else {
s.toInt
}
}
def parseSymbolAndBonus(token: String): (String, Int) = {
if (token.contains("+")) {
token.split("\\+", 2) match {
case Array(n, b) => n.trim -> parseNumber(b)
}
} else if (token.contains("-")) {
token.split("-", 2) match {
case Array(n, b) => n.trim -> -parseNumber(b)
}
} else {
token.trim -> 0
}
}
}