mirror of
https://github.com/irmen/prog8.git
synced 2024-06-13 09:29:34 +00:00
260 lines
12 KiB
Kotlin
260 lines
12 KiB
Kotlin
package prog8
|
|
|
|
import kotlinx.cli.*
|
|
import prog8.ast.base.AstException
|
|
import prog8.code.core.CbmPrgLauncherType
|
|
import prog8.code.core.toHex
|
|
import prog8.code.target.*
|
|
import prog8.code.target.virtual.VirtualMachineDefinition
|
|
import prog8.compiler.CompilationResult
|
|
import prog8.compiler.CompilerArguments
|
|
import prog8.compiler.compileProgram
|
|
import java.io.File
|
|
import java.nio.file.FileSystems
|
|
import java.nio.file.Path
|
|
import java.nio.file.StandardWatchEventKinds
|
|
import java.nio.file.WatchKey
|
|
import java.time.LocalDateTime
|
|
import kotlin.io.path.Path
|
|
import kotlin.system.exitProcess
|
|
|
|
|
|
fun main(args: Array<String>) {
|
|
val buildVersion = object {}.javaClass.getResource("/version.txt")?.readText()?.trim()
|
|
println("\nProg8 compiler v$buildVersion by Irmen de Jong (irmen@razorvine.net)")
|
|
println("This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html\n")
|
|
|
|
val succes = compileMain(args)
|
|
if(!succes)
|
|
exitProcess(1)
|
|
}
|
|
|
|
|
|
fun pathFrom(stringPath: String, vararg rest: String): Path = FileSystems.getDefault().getPath(stringPath, *rest)
|
|
|
|
|
|
private fun compileMain(args: Array<String>): Boolean {
|
|
val cli = ArgParser("prog8compiler", prefixStyle = ArgParser.OptionPrefixStyle.JVM)
|
|
val asmListfile by cli.option(ArgType.Boolean, fullName = "asmlist", description = "make the assembler produce a listing file as well")
|
|
val symbolDefs by cli.option(ArgType.String, fullName = "D", description = "define assembly symbol(s) with -D SYMBOL=VALUE").multiple()
|
|
val evalStackAddrString by cli.option(ArgType.String, fullName = "esa", description = "override the eval-stack base address (must be page aligned)")
|
|
val startEmulator1 by cli.option(ArgType.Boolean, fullName = "emu", description = "auto-start emulator after successful compilation")
|
|
val startEmulator2 by cli.option(ArgType.Boolean, fullName = "emu2", description = "auto-start alternative emulator after successful compilation")
|
|
val experimentalCodegen by cli.option(ArgType.Boolean, fullName = "expericodegen", description = "use experimental/alternative codegen")
|
|
val dontWriteAssembly by cli.option(ArgType.Boolean, fullName = "noasm", description="don't create assembly code")
|
|
val dontOptimize by cli.option(ArgType.Boolean, fullName = "noopt", description = "don't perform any optimizations")
|
|
val outputDir by cli.option(ArgType.String, fullName = "out", description = "directory for output files instead of current directory").default(".")
|
|
val optimizeFloatExpressions by cli.option(ArgType.Boolean, fullName = "optfloatx", description = "optimize float expressions (warning: can increase program size)")
|
|
val quietAssembler by cli.option(ArgType.Boolean, fullName = "quietasm", description = "don't print assembler output results")
|
|
val slowCodegenWarnings by cli.option(ArgType.Boolean, fullName = "slowwarn", description="show debug warnings about slow/problematic assembly code generation")
|
|
val sourceDirs by cli.option(ArgType.String, fullName="srcdirs", description = "list of extra paths, separated with ${File.pathSeparator}, to search in for imported modules").multiple().delimiter(File.pathSeparator)
|
|
val compilationTarget by cli.option(ArgType.String, fullName = "target", description = "target output of the compiler (one of '${C64Target.NAME}', '${C128Target.NAME}', '${Cx16Target.NAME}', '${AtariTarget.NAME}', '${VMTarget.NAME}')").required()
|
|
val startVm by cli.option(ArgType.Boolean, fullName = "vm", description = "load and run a .p8ir IR source file in the VM")
|
|
val watchMode by cli.option(ArgType.Boolean, fullName = "watch", description = "continuous compilation mode (watch for file changes)")
|
|
val varsHigh by cli.option(ArgType.Boolean, fullName = "varshigh", description = "put uninitialized variables in high memory area instead of at the end of the program")
|
|
val useNewExprCode by cli.option(ArgType.Boolean, fullName = "newexpr", description = "use new expression code-gen (experimental)")
|
|
val splitWordArrays by cli.option(ArgType.Boolean, fullName = "splitarrays", description = "treat all word arrays as tagged with @split to make them lsb/msb split in memory")
|
|
val moduleFiles by cli.argument(ArgType.String, fullName = "modules", description = "main module file(s) to compile").multiple(999)
|
|
|
|
try {
|
|
cli.parse(args)
|
|
} catch (e: IllegalStateException) {
|
|
System.err.println(e.message)
|
|
return false
|
|
}
|
|
|
|
val outputPath = pathFrom(outputDir)
|
|
if(!outputPath.toFile().isDirectory) {
|
|
System.err.println("Output path doesn't exist")
|
|
return false
|
|
}
|
|
|
|
val faultyOption = moduleFiles.firstOrNull { it.startsWith('-') }
|
|
if(faultyOption!=null) {
|
|
System.err.println("Unknown command line option given: $faultyOption")
|
|
return false
|
|
}
|
|
|
|
val srcdirs = sourceDirs.toMutableList()
|
|
if(srcdirs.firstOrNull()!=".")
|
|
srcdirs.add(0, ".")
|
|
|
|
if (compilationTarget !in setOf(C64Target.NAME, C128Target.NAME, Cx16Target.NAME, AtariTarget.NAME, VMTarget.NAME)) {
|
|
System.err.println("Invalid compilation target: $compilationTarget")
|
|
return false
|
|
}
|
|
|
|
if(startVm==true) {
|
|
return runVm(moduleFiles.first())
|
|
}
|
|
|
|
var evalStackAddr: UInt? = null
|
|
if(evalStackAddrString!=null) {
|
|
try {
|
|
evalStackAddr = if (evalStackAddrString!!.startsWith("0x"))
|
|
evalStackAddrString!!.substring(2).toUInt(16)
|
|
else if (evalStackAddrString!!.startsWith("$"))
|
|
evalStackAddrString!!.substring(1).toUInt(16)
|
|
else
|
|
evalStackAddrString!!.toUInt()
|
|
} catch(nx: NumberFormatException) {
|
|
System.err.println("invalid address for evalstack: $evalStackAddrString")
|
|
return false
|
|
}
|
|
if(evalStackAddr !in 256u..65536u-512u || (evalStackAddr and 255u != 0u)) {
|
|
System.err.println("invalid address for evalstack: ${evalStackAddr.toHex()}")
|
|
return false
|
|
}
|
|
}
|
|
|
|
val processedSymbols = processSymbolDefs(symbolDefs) ?: return false
|
|
|
|
if(watchMode==true) {
|
|
val watchservice = FileSystems.getDefault().newWatchService()
|
|
val allImportedFiles = mutableSetOf<Path>()
|
|
val results = mutableListOf<CompilationResult>()
|
|
while(true) {
|
|
println("Continuous watch mode active. Modules: $moduleFiles")
|
|
results.clear()
|
|
for(filepathRaw in moduleFiles) {
|
|
val filepath = pathFrom(filepathRaw).normalize()
|
|
val compilerArgs = CompilerArguments(
|
|
filepath,
|
|
dontOptimize != true,
|
|
optimizeFloatExpressions == true,
|
|
dontWriteAssembly != true,
|
|
slowCodegenWarnings == true,
|
|
quietAssembler == true,
|
|
asmListfile == true,
|
|
experimentalCodegen == true,
|
|
varsHigh == true,
|
|
useNewExprCode == true,
|
|
compilationTarget,
|
|
evalStackAddr,
|
|
splitWordArrays == true,
|
|
processedSymbols,
|
|
srcdirs,
|
|
outputPath
|
|
)
|
|
val compilationResult = compileProgram(compilerArgs)
|
|
if(compilationResult!=null)
|
|
results.add(compilationResult)
|
|
}
|
|
|
|
val allNewlyImportedFiles = results.flatMap { it.importedFiles }
|
|
allImportedFiles.addAll(allNewlyImportedFiles)
|
|
|
|
println("Imported files (now watching:)")
|
|
for (importedFile in allImportedFiles) {
|
|
print(" ")
|
|
println(importedFile)
|
|
val watchDir = importedFile.parent ?: Path("")
|
|
watchDir.register(watchservice, StandardWatchEventKinds.ENTRY_MODIFY)
|
|
}
|
|
println("[${LocalDateTime.now().withNano(0)}] Waiting for file changes.")
|
|
|
|
fun determineRecompilationNeeded(event: WatchKey): Boolean {
|
|
if(event.isValid) {
|
|
for (changed in event.pollEvents()) {
|
|
if (changed.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
|
|
val changedPath = changed.context() as Path
|
|
if (allImportedFiles.any { it.fileName == changedPath.fileName }) {
|
|
println(" change detected: $changedPath")
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var recompile=false
|
|
while(!recompile) {
|
|
val event = watchservice.take()
|
|
Thread.sleep(50) // avoid multiple events on same file
|
|
recompile = determineRecompilationNeeded(event)
|
|
event.reset()
|
|
}
|
|
|
|
println("\u001b[H\u001b[2J") // clear the screen
|
|
}
|
|
|
|
} else {
|
|
for(filepathRaw in moduleFiles) {
|
|
val filepath = pathFrom(filepathRaw).normalize()
|
|
val compilationResult: CompilationResult
|
|
try {
|
|
val compilerArgs = CompilerArguments(
|
|
filepath,
|
|
dontOptimize != true,
|
|
optimizeFloatExpressions == true,
|
|
dontWriteAssembly != true,
|
|
slowCodegenWarnings == true,
|
|
quietAssembler == true,
|
|
asmListfile == true,
|
|
experimentalCodegen == true,
|
|
varsHigh == true,
|
|
useNewExprCode == true,
|
|
compilationTarget,
|
|
evalStackAddr,
|
|
splitWordArrays == true,
|
|
processedSymbols,
|
|
srcdirs,
|
|
outputPath
|
|
)
|
|
val result = compileProgram(compilerArgs)
|
|
if(result==null)
|
|
return false
|
|
else
|
|
compilationResult = result
|
|
} catch (x: AstException) {
|
|
return false
|
|
}
|
|
|
|
if(startEmulator1==true || startEmulator2==true) {
|
|
if (compilationResult.compilerAst.name.isEmpty()) {
|
|
println("\nCan't start emulator because no program was assembled.")
|
|
return true
|
|
}
|
|
}
|
|
|
|
val programNameInPath = outputPath.resolve(compilationResult.compilerAst.name)
|
|
|
|
if(startEmulator1==true || startEmulator2==true) {
|
|
if (compilationResult.compilationOptions.launcher != CbmPrgLauncherType.NONE || compilationTarget=="atari") {
|
|
if (startEmulator1 == true)
|
|
compilationResult.compilationOptions.compTarget.machine.launchEmulator(1, programNameInPath)
|
|
else if (startEmulator2 == true)
|
|
compilationResult.compilationOptions.compTarget.machine.launchEmulator(2, programNameInPath)
|
|
} else {
|
|
println("\nCan't start emulator because program has no launcher type.")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
private fun processSymbolDefs(symbolDefs: List<String>): Map<String, String>? {
|
|
val result = mutableMapOf<String, String>()
|
|
val defPattern = """(.+)\s*=\s*(.+)""".toRegex()
|
|
for(def in symbolDefs) {
|
|
val match = defPattern.matchEntire(def.trim())
|
|
if(match==null) {
|
|
System.err.println("invalid symbol definition (expected NAME=VALUE): $def")
|
|
return null
|
|
}
|
|
val (_, name, value) = match.groupValues
|
|
result[name.trim()] = value.trim()
|
|
}
|
|
return result
|
|
}
|
|
|
|
fun runVm(irFilename: String): Boolean {
|
|
val irFile = Path(irFilename)
|
|
val vmdef = VirtualMachineDefinition()
|
|
vmdef.launchEmulator(0, irFile)
|
|
return true
|
|
}
|