diff --git a/.agents/skills/prog8-coder/SKILL.md b/.agents/skills/prog8-coder/SKILL.md index 6e90deef6..f2dc7627f 100644 --- a/.agents/skills/prog8-coder/SKILL.md +++ b/.agents/skills/prog8-coder/SKILL.md @@ -34,6 +34,9 @@ repeating them here: - Use `%zeropage basicsafe` in test programs when a clean return is needed. - Find library routines with `prog8c -libsearch ` or extract them with `prog8c -libdump `. +- Options accept single-dash or double-dash (`-target` == `--target`). Use + `-out ` to place output in a directory, or `-o ` to name the exact + final program artifact (all other output files then land beside it). For CX16 emulator programs, use `%encoding iso`, call `txt.iso()` in `start`, and finish with `sys.poweroff_system()`. CBM targets use PETSCII by default; diff --git a/AGENTS.md b/AGENTS.md index 1a2bddc31..401e646e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -226,13 +226,15 @@ When the task involves writing or understanding `.p8` (Prog8 source) or `.p8ir` - the prog8c compiler executable can be found in the compiler/build/install/prog8c/bin folder (this is already added to the shell's path) - **the `-check` switch performs a quick syntax/semantic check only; it will NOT produce any output files (no .prg, .asm, etc.)**. Use it only for fast error checking during development. - **the `-noopt` switch DISABLES all optimizations** - useful for debugging to determine if a problem is caused by the optimizer. **Optimizations are ENABLED by default** (no flag needed). -- **prog8c uses single-dash command line options** (e.g., `-target`, `-noopt`, `-check`), NOT double-dash (`--target` is invalid). +- **prog8c options accept single-dash or double-dash** (e.g., `-target` and `--target` are equivalent). - **the `-printast1` switch prints out the internal Compiler AST** after parsing and semantic analysis. - **the `-printast2` switch prints out the optimized Simple AST** just before it goes to the code generator. This is useful for debugging optimizer issues. -- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the same directory as the source file**. +- **the `-out outdir` switch sets an alternative output directory** for compiled files (.prg, .asm, .list, etc.). **By default, output files are written to the current working directory**. +- **the `-o ` switch names the exact final program artifact** (also spelled `--output`); all other output files go in the same directory with the same stem. Cannot be combined with `-out` or with multiple source files. - **Other useful flags**: `-quiet` (suppress messages), `-warnimplicitcasts` (warn on implicit type widening), `-daemon` (keep a background compiler process alive — must be passed on every invocation) ### Compilation Output Files +Names follow the source file name, or the stem of the `-o` file when that option is used. - `*.prg` - The final compiled program file for the target system (e.g., Commander X16) - `*.asm` - Generated assembly code from the Prog8 source - `*.list` - Generated full assembly listing file from the Prog8 source diff --git a/codeCore/src/prog8/code/assembly/AssemblyProgram6502.kt b/codeCore/src/prog8/code/assembly/AssemblyProgram6502.kt index 6db8bcf12..57cc477a3 100644 --- a/codeCore/src/prog8/code/assembly/AssemblyProgram6502.kt +++ b/codeCore/src/prog8/code/assembly/AssemblyProgram6502.kt @@ -1,10 +1,7 @@ package prog8.code.assembly import prog8.code.GENERATED_LABEL_PREFIX -import prog8.code.core.CompilationOptions -import prog8.code.core.ICompilationTarget -import prog8.code.core.IErrorReporter -import prog8.code.core.OutputType +import prog8.code.core.* import prog8.code.target.C128Target import prog8.code.target.C64Target import prog8.code.target.PETTarget @@ -13,25 +10,20 @@ import java.nio.file.Path class AssemblyProgram6502( override val name: String, - outputDir: Path, + private val options: CompilationOptions, private val compTarget: ICompilationTarget) : IAssemblyProgram { override val irInstructionCount: Int = 0 override val irChunkCount: Int = 0 override val irRegisterCount: Int = 0 - private val assemblyFile = outputDir.resolve("$name.asm") - private val prgFile = outputDir.resolve("$name.prg") // CBM prg executable program - private val xexFile = outputDir.resolve("$name.xex") // Atari xex executable program - private val binFile = outputDir.resolve("$name.bin") - private val viceMonListFile = outputDir.resolve(C64Target.viceMonListName(name)) - private val listFile = outputDir.resolve("$name.list") - override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean { val assemblerCommand: List + val outs = OutputFiles.of(options, name) fun addRemainingOptions(command: MutableList, program: Path, assembly: Path): List { + // keep additionalAssemblerOptions BEFORE --output: a target config may contain its own --output if(options.compTarget.additionalAssemblerOptions.isNotEmpty()) command.addAll(options.compTarget.additionalAssemblerOptions) @@ -44,7 +36,7 @@ class AssemblyProgram6502( // CBM machines .prg generation. val command = mutableListOf("64tass", "--cbm-prg", "--ascii", "--case-sensitive", "--long-branch", - "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile") + "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}") if(options.warnSymbolShadowing) command.add("-Wshadow") @@ -55,10 +47,10 @@ class AssemblyProgram6502( command.add("--quiet") if(options.asmListfile) { - command.add("--list=$listFile") + command.add("--list=${outs.list()}") } - assemblerCommand = addRemainingOptions(command, prgFile, assemblyFile) + assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm()) if(!options.quiet) println("\nCreating prg for target ${compTarget.name}.") } @@ -66,7 +58,7 @@ class AssemblyProgram6502( // Atari800XL .xex generation. val command = mutableListOf("64tass", "--atari-xex", "--case-sensitive", "--long-branch", - "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile") + "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}") if(options.warnSymbolShadowing) command.add("-Wshadow") @@ -77,16 +69,16 @@ class AssemblyProgram6502( command.add("--quiet") if(options.asmListfile) - command.add("--list=$listFile") + command.add("--list=${outs.list()}") - assemblerCommand = addRemainingOptions(command,xexFile, assemblyFile) + assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm()) if(!options.quiet) println("\nCreating xex for target ${compTarget.name}.") } OutputType.RAW -> { // Neo6502/headerless raw program generation. val command = mutableListOf("64tass", "--nostart", "--case-sensitive", "--long-branch", - "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile") + "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}") if(options.warnSymbolShadowing) command.add("-Wshadow") @@ -97,9 +89,9 @@ class AssemblyProgram6502( command.add("--quiet") if(options.asmListfile) - command.add("--list=$listFile") + command.add("--list=${outs.list()}") - assemblerCommand = addRemainingOptions(command, binFile, assemblyFile) + assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm()) if(!options.quiet) println("\nCreating raw binary for target ${compTarget.name}.") } @@ -107,7 +99,7 @@ class AssemblyProgram6502( // CBM machines library (.bin) generation (with or without 2 byte load address header depending on the compilation target machine) val command = mutableListOf("64tass", "--ascii", "--case-sensitive", "--long-branch", - "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=$viceMonListFile") + "-Wall", "-Wno-implied-reg", "--no-monitor", "--dump-labels", "--vice-labels", "--labels=${outs.viceMonList()}") if(options.warnSymbolShadowing) command.add("-Wshadow") @@ -118,7 +110,7 @@ class AssemblyProgram6502( command.add("--quiet") if(options.asmListfile) - command.add("--list=$listFile") + command.add("--list=${outs.list()}") if(compTarget.name in listOf(C64Target.NAME, C128Target.NAME, PETTarget.NAME)) { if(!options.quiet) @@ -130,7 +122,7 @@ class AssemblyProgram6502( command.add("--nostart") // should be headerless bin, because basic has problems doing a normal LOAD"lib",8,1 - need to use BLOAD } - assemblerCommand = addRemainingOptions(command, binFile, assemblyFile) + assemblerCommand = addRemainingOptions(command, outs.programFile, outs.asm()) } else -> error("Unsupported output type: ${compTarget.defaultOutputType}") } @@ -159,16 +151,16 @@ class AssemblyProgram6502( val result = process.waitFor() if (result == 0) { - removeGeneratedLabelsFromMonlist() - generateBreakpointList() + removeGeneratedLabelsFromMonlist(outs) + generateBreakpointList(outs) } return result==0 } - private fun removeGeneratedLabelsFromMonlist() { + private fun removeGeneratedLabelsFromMonlist(outs: OutputFiles) { val pattern = Regex("""al (\w+) \S+$GENERATED_LABEL_PREFIX.+?""") - val lines = viceMonListFile.toFile().readLines() - viceMonListFile.toFile().outputStream().bufferedWriter().use { + val lines = outs.viceMonList().toFile().readLines() + outs.viceMonList().toFile().outputStream().bufferedWriter().use { for (line in lines) { if(pattern.matchEntire(line)==null) it.write(line+"\n") @@ -176,11 +168,11 @@ class AssemblyProgram6502( } } - private fun generateBreakpointList() { + private fun generateBreakpointList(outs: OutputFiles) { // builds list of breakpoints, appends to monitor list file val breakpoints = mutableListOf() val pattern = Regex("""al (\w+) \S+_prog8_breakpoint_\d+.?""") // gather breakpoints by the source label that's generated for them - for (line in viceMonListFile.toFile().readLines()) { + for (line in outs.viceMonList().toFile().readLines()) { val match = pattern.matchEntire(line) if (match != null) breakpoints.add("break $" + match.groupValues[1]) @@ -189,7 +181,7 @@ class AssemblyProgram6502( breakpoints.add(0, "; breakpoint list now follows") breakpoints.add(1, "; $num breakpoints have been defined") breakpoints.add(2, "del") - viceMonListFile.toFile().appendText(breakpoints.joinToString("\n") + "\n") + outs.viceMonList().toFile().appendText(breakpoints.joinToString("\n") + "\n") } } diff --git a/codeCore/src/prog8/code/core/CompilationOptions.kt b/codeCore/src/prog8/code/core/CompilationOptions.kt index 9c3b2aa9e..3b6c49599 100644 --- a/codeCore/src/prog8/code/core/CompilationOptions.kt +++ b/codeCore/src/prog8/code/core/CompilationOptions.kt @@ -36,7 +36,8 @@ class CompilationOptions(val output: OutputType, var quiet: Boolean = false, var profilingInstrumentation: Boolean = false, var symbolDefs: Map = emptyMap(), - var privateSymbols: Boolean = false + var privateSymbols: Boolean = false, + var outputFile: Path? = null ) { init { compTarget.initializeMemoryAreas(this) @@ -77,6 +78,7 @@ class CompilationOptions(val output: OutputType, private var profilingInstrumentation: Boolean = false private var newCodegen: Boolean = false private var symbolDefs: Map = emptyMap() + private var outputFile: Path? = null fun output(output: OutputType) = apply { this.output = output } fun launcher(launcher: CbmPrgLauncherType) = apply { this.launcher = launcher } @@ -107,6 +109,7 @@ class CompilationOptions(val output: OutputType, fun profilingInstrumentation(profilingInstrumentation: Boolean) = apply { this.profilingInstrumentation = profilingInstrumentation } fun symbolDefs(symbolDefs: Map) = apply { this.symbolDefs = symbolDefs } fun newCodegen(newCodegen: Boolean) = apply { this.newCodegen = newCodegen } + fun outputFile(outputFile: Path?) = apply { this.outputFile = outputFile } fun build(): CompilationOptions { return CompilationOptions( @@ -115,7 +118,7 @@ class CompilationOptions(val output: OutputType, includeSourcelines, dumpVariables, dumpSymbols, newCodegen, varsHighBank, varsGolden, breakpointCpuInstruction, ignoreFootguns, outputDir, quiet, profilingInstrumentation, symbolDefs - ) + ).apply { this.outputFile = this@Builder.outputFile } } } } diff --git a/codeCore/src/prog8/code/core/ICompilationTarget.kt b/codeCore/src/prog8/code/core/ICompilationTarget.kt index c9634c9f1..a0b3359a3 100644 --- a/codeCore/src/prog8/code/core/ICompilationTarget.kt +++ b/codeCore/src/prog8/code/core/ICompilationTarget.kt @@ -93,7 +93,12 @@ interface ICompilationTarget: IStringEncoding, IMemSizer { fun convertFloatToBytes(num: Double): List fun convertBytesToFloat(bytes: List): Double - fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) + /** + * Launches an emulator to run the compiled program artifact. + * [programFile] is the exact artifact path including its extension; launchers must use it + * verbatim, without stripping or appending one. + */ + fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) fun isIOAddress(address: UInt): Boolean override fun encodeString(str: String, encoding: Encoding): List diff --git a/codeCore/src/prog8/code/core/OutputFiles.kt b/codeCore/src/prog8/code/core/OutputFiles.kt new file mode 100644 index 000000000..c1378c771 --- /dev/null +++ b/codeCore/src/prog8/code/core/OutputFiles.kt @@ -0,0 +1,79 @@ +package prog8.code.core + +import prog8.code.target.VMTarget +import java.nio.file.Path +import kotlin.io.path.extension +import kotlin.io.path.name + + +/** VICE/box16 monitor (symbol) list file name for an artifact stem. */ +fun viceMonListName(stem: String) = "$stem.vice-mon-list" + + +/** + * Artifact stem of a user-supplied `-o` output file name. + * A dot at index 0 is not an extension separator: ".pgz" keeps its whole name. + * Only applied to `-o`; the legacy program stem (source name without extension) is used verbatim. + */ +fun outputStemOf(name: Path): String { + val filename = name.name + val dotIndex = filename.lastIndexOf('.') + return if (dotIndex <= 0) filename else filename.substring(0, dotIndex) +} + +/** + * The final program artifact path: [CompilationOptions.outputFile] verbatim when set, + * otherwise [defaultName] plus the extension for [CompilationOptions.output], in [CompilationOptions.outputDir]. + * RAW and LIBRARY both use ".bin"; AMIGAHUNK has no extension. + * On the virtual target the artifact is the IR file, so ".p8ir" is always enforced on -o. + */ +fun CompilationOptions.programFile(defaultName: String): Path { + // VMTarget's defaultOutputType = PRG is misleading: its artifact is the .p8ir file, never PRG. + // The VM identifies the IR by its extension, so -o is not verbatim here: ".p8ir" is enforced. + if(compTarget is VMTarget) { + val ir = outputFile ?: return outputDir.resolve("$defaultName.p8ir") + return if (ir.extension.lowercase()=="p8ir") ir else ir.resolveSibling("${ir.name}.p8ir") + } + outputFile?.let { return it } + val filename = when(output) { + OutputType.PRG -> "$defaultName.prg" + OutputType.XEX -> "$defaultName.xex" + OutputType.RAW -> "$defaultName.bin" + OutputType.LIBRARY -> "$defaultName.bin" + OutputType.ELF -> "$defaultName.elf" + OutputType.AMIGAHUNK -> defaultName + } + return outputDir.resolve(filename) +} + +/** + * Output artifact naming. + * Invariant: [asm] must resolve into the same directory as `CompilationOptions.outputDir`, + * because 64tass resolves `.binary` includes relative to the generated `.asm`. + */ +class OutputFiles(val programFile: Path, val stem: String, val dir: Path) { + fun asm() = dir.resolve("$stem.asm") + fun list() = dir.resolve("$stem.list") + fun viceMonList() = dir.resolve(viceMonListName(stem)) + fun binFile() = dir.resolve("$stem.bin") + fun ir() = dir.resolve("$stem.p8ir") + fun bankedCalls() = dir.resolve("$stem.bankedcalls") + fun obj() = dir.resolve("$stem.o") + fun linkScript() = dir.resolve("$stem.link.ld") + + companion object { + /** + * Builds the output file set. Without `-o` the stem is [defaultName] verbatim + * (outputStemOf is only applied to the `-o` name). With `-o` the stem is derived + * from the resolved artifact name, so it also covers the virtual target appending + * ".p8ir". [dir] is the program file's parent, or [CompilationOptions.outputDir] + * for a bare -o filename. + */ + fun of(options: CompilationOptions, defaultName: String): OutputFiles { + val programFile = options.programFile(defaultName) + val stem = options.outputFile?.let { outputStemOf(programFile) } ?: defaultName + val dir = programFile.parent ?: options.outputDir + return OutputFiles(programFile, stem, dir) + } + } +} \ No newline at end of file diff --git a/codeCore/src/prog8/code/target/Amiga1200Target.kt b/codeCore/src/prog8/code/target/Amiga1200Target.kt index f5d1e2b58..8006ff752 100644 --- a/codeCore/src/prog8/code/target/Amiga1200Target.kt +++ b/codeCore/src/prog8/code/target/Amiga1200Target.kt @@ -55,7 +55,7 @@ class Amiga1200Target: ICompilationTarget, TODO("convert bytes to float") } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { if(selectedEmulator!=1) { System.err.println("The amiga1200 target only supports the main emulator (Amitools's vamos).") return @@ -65,12 +65,11 @@ class Amiga1200Target: ICompilationTarget, CpuType.M68020 -> "68020" else -> error("invalid cpu type") } - val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}") val cmd = listOf( "vamos", "--cpu", cpuStr, - exeFile.toString(), + programFile.toString(), ) if(!quiet) println("Launching Amitools's Vamos...") diff --git a/codeCore/src/prog8/code/target/Amiga500Target.kt b/codeCore/src/prog8/code/target/Amiga500Target.kt index bbb011f6d..541e21ddb 100644 --- a/codeCore/src/prog8/code/target/Amiga500Target.kt +++ b/codeCore/src/prog8/code/target/Amiga500Target.kt @@ -95,7 +95,7 @@ class Amiga500Target: ICompilationTarget, TODO("convert bytes to float") } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { if(selectedEmulator!=1) { System.err.println("The amiga500 target only supports the main emulator (Amitools's vamos).") return @@ -105,12 +105,11 @@ class Amiga500Target: ICompilationTarget, CpuType.M68020 -> "68020" else -> error("invalid cpu type") } - val exeFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}") val cmd = listOf( "vamos", "--cpu", cpuStr, - exeFile.toString(), + programFile.toString(), ) if(!quiet) println("Launching Amitools's Vamos...") diff --git a/codeCore/src/prog8/code/target/C128Target.kt b/codeCore/src/prog8/code/target/C128Target.kt index 489593aff..7e9038303 100644 --- a/codeCore/src/prog8/code/target/C128Target.kt +++ b/codeCore/src/prog8/code/target/C128Target.kt @@ -28,7 +28,7 @@ class C128Target: ICompilationTarget, override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE - override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt() + override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE override val POINTER_MEM_SIZE = 2u override val ARRAY_SIZE_LIMIT = 256u override val PROGRAM_LOAD_ADDRESS = 0x1c01u @@ -54,7 +54,7 @@ class C128Target: ICompilationTarget, return m5.toDouble() } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { if(selectedEmulator!=1) { System.err.println("The c128 target only supports the main emulator (Vice).") return @@ -63,9 +63,9 @@ class C128Target: ICompilationTarget, if(!quiet) println("\nStarting C-128 emulator x128...") - val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString()) - val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist, - "-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg") + val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile))) + val cmdline = listOf("x128", "-silent", "-moncommands", viceMonlist.toString(), + "-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString()) val processb = ProcessBuilder(cmdline) if(!quiet) processb.inheritIO() diff --git a/codeCore/src/prog8/code/target/C64Target.kt b/codeCore/src/prog8/code/target/C64Target.kt index e2406e56c..5586b5299 100644 --- a/codeCore/src/prog8/code/target/C64Target.kt +++ b/codeCore/src/prog8/code/target/C64Target.kt @@ -22,8 +22,6 @@ class C64Target: ICompilationTarget, companion object { const val NAME = "c64" - - fun viceMonListName(baseFilename: String) = "$baseFilename.vice-mon-list" } @@ -58,7 +56,7 @@ class C64Target: ICompilationTarget, return m5.toDouble() } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { if(selectedEmulator!=1) { System.err.println("The c64 target only supports the main emulator (Vice).") return @@ -68,9 +66,9 @@ class C64Target: ICompilationTarget, if(!quiet) println("\nStarting C-64 emulator $emulator...") - val viceMonlist = viceMonListName(programNameWithPath.toString()) - val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist, - "-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg") + val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile))) + val cmdline = listOf(emulator, "-silent", "-moncommands", viceMonlist.toString(), + "-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString()) val processb = ProcessBuilder(cmdline) if(!quiet) processb.inheritIO() diff --git a/codeCore/src/prog8/code/target/ConfigFileTarget.kt b/codeCore/src/prog8/code/target/ConfigFileTarget.kt index 460ba5650..e611d4892 100644 --- a/codeCore/src/prog8/code/target/ConfigFileTarget.kt +++ b/codeCore/src/prog8/code/target/ConfigFileTarget.kt @@ -182,7 +182,7 @@ class ConfigFileTarget( override fun getFloatAsmBytes(num: Number) = TODO("floats") override fun convertFloatToBytes(num: Double): List = TODO("floats") override fun convertBytesToFloat(bytes: List): Double = TODO("floats") - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { throw IllegalArgumentException("Custom compiler target cannot automatically launch an emulator. Do this manually.") } diff --git a/codeCore/src/prog8/code/target/Cx16Target.kt b/codeCore/src/prog8/code/target/Cx16Target.kt index 71e7e4fef..d509cb42d 100644 --- a/codeCore/src/prog8/code/target/Cx16Target.kt +++ b/codeCore/src/prog8/code/target/Cx16Target.kt @@ -28,7 +28,7 @@ class Cx16Target: ICompilationTarget, override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE - override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt() + override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE override val POINTER_MEM_SIZE = 2u override val ARRAY_SIZE_LIMIT = 256u override val PROGRAM_LOAD_ADDRESS = 0x0801u @@ -54,7 +54,7 @@ class Cx16Target: ICompilationTarget, return m5.toDouble() } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { val emulator: String val extraArgs: List @@ -65,7 +65,7 @@ class Cx16Target: ICompilationTarget, } 2 -> { emulator = "box16" - extraArgs = listOf("-sym", C64Target.viceMonListName(programNameWithPath.toString())) + extraArgs = listOf("-sym", programFile.resolveSibling(viceMonListName(outputStemOf(programFile))).toString()) } else -> { System.err.println("Cx16 target only supports x16emu and box16 emulators.") @@ -76,7 +76,7 @@ class Cx16Target: ICompilationTarget, if(!quiet) println("\nStarting Commander X16 emulator $emulator...") - val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", "${programNameWithPath}.prg") + extraArgs + val cmdline = listOf(emulator, "-scale", "2", "-rtc", "-run", "-prg", programFile.toString()) + extraArgs val processb = ProcessBuilder(cmdline) if(!quiet) processb.inheritIO() diff --git a/codeCore/src/prog8/code/target/PETTarget.kt b/codeCore/src/prog8/code/target/PETTarget.kt index 9e9d1b166..daad58ee2 100644 --- a/codeCore/src/prog8/code/target/PETTarget.kt +++ b/codeCore/src/prog8/code/target/PETTarget.kt @@ -27,7 +27,7 @@ class PETTarget: ICompilationTarget, override val FLOAT_MAX_POSITIVE = Mflpt5.FLOAT_MAX_POSITIVE override val FLOAT_MAX_NEGATIVE = Mflpt5.FLOAT_MAX_NEGATIVE - override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE.toUInt() + override val FLOAT_MEM_SIZE = Mflpt5.FLOAT_MEM_SIZE override val POINTER_MEM_SIZE = 2u override val ARRAY_SIZE_LIMIT = 256u override val PROGRAM_LOAD_ADDRESS = 0x0401u @@ -53,7 +53,7 @@ class PETTarget: ICompilationTarget, return m5.toDouble() } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { if(selectedEmulator!=1) { System.err.println("The pet target only supports the main emulator (Vice).") return @@ -62,9 +62,9 @@ class PETTarget: ICompilationTarget, if(!quiet) println("\nStarting PET emulator...") - val viceMonlist = C64Target.viceMonListName(programNameWithPath.toString()) - val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist, - "-autostartprgmode", "1", "-autostart-warp", "-autostart", "${programNameWithPath}.prg") + val viceMonlist = programFile.resolveSibling(viceMonListName(outputStemOf(programFile))) + val cmdline = listOf("xpet", "-model", "4032", "-ramsize", "32", "-videosize", "40", "-silent", "-moncommands", viceMonlist.toString(), + "-autostartprgmode", "1", "-autostart-warp", "-autostart", programFile.toString()) val processb = ProcessBuilder(cmdline) if(!quiet) processb.inheritIO() diff --git a/codeCore/src/prog8/code/target/Qemu68kTarget.kt b/codeCore/src/prog8/code/target/Qemu68kTarget.kt index b5ee655fd..54c546618 100644 --- a/codeCore/src/prog8/code/target/Qemu68kTarget.kt +++ b/codeCore/src/prog8/code/target/Qemu68kTarget.kt @@ -56,42 +56,27 @@ class Qemu68kTarget: ICompilationTarget, TODO("convert bytes to float") } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { if(selectedEmulator!=1) { System.err.println("The qemu68k target only supports the main emulator (Qemu).") return } - val elfFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.elf") - val binFile = programNameWithPath.resolveSibling("${programNameWithPath.fileName}.bin") - val isElf = elfFile.toFile().exists() - val cpuStr = this.cpu.toString().lowercase() - - val cmd = if (isElf) { - listOf( - "qemu-system-m68k", - "-M", "virt", - "-cpu", cpuStr, - "-m", "1M", - "-kernel", elfFile.toString(), - "-nographic" - ) - } else if (binFile.toFile().exists()) { - val loadAddr = PROGRAM_LOAD_ADDRESS.toInt() - listOf( - "qemu-system-m68k", - "-M", "virt", - "-cpu", cpuStr, - "-m", "1M", - "-device", "loader,file=${binFile},addr=0x${loadAddr.toString(16)},cpu-num=0", - "-nographic" - ) - } else { - System.err.println("No .elf or .bin file found for ${programNameWithPath.fileName}") + if(!programFile.toFile().exists()) { + System.err.println("No program file found: $programFile") return } + val cpuStr = this.cpu.toString().lowercase() + + val cmd = listOf( + "qemu-system-m68k", + "-M", "virt", + "-cpu", cpuStr, + "-m", "1M", + "-kernel", programFile.toString(), + "-nographic" + ) if(!quiet) { - val launchMsg = if (isElf) "ELF" else "raw binary" - println("Launching QEMU (press Ctrl-A X to exit)... (from $launchMsg)") + println("Launching QEMU (press Ctrl-A X to exit)...") } val pb = ProcessBuilder(cmd).inheritIO() try { diff --git a/codeCore/src/prog8/code/target/VMTarget.kt b/codeCore/src/prog8/code/target/VMTarget.kt index e9c1640e0..203892993 100644 --- a/codeCore/src/prog8/code/target/VMTarget.kt +++ b/codeCore/src/prog8/code/target/VMTarget.kt @@ -3,9 +3,7 @@ package prog8.code.target import prog8.code.core.* import prog8.code.target.encodings.Encoder import java.nio.file.Path -import kotlin.io.path.extension import kotlin.io.path.isReadable -import kotlin.io.path.name import kotlin.io.path.readText class VMTarget: ICompilationTarget, @@ -71,21 +69,24 @@ class VMTarget: ICompilationTarget, return Double.fromBits(b0 or b1 or b2 or b3 or b4 or b5 or b6 or b7) } - override fun launchEmulator(selectedEmulator: Int, programNameWithPath: Path, quiet: Boolean) { - launchEmulatorWithTrace(programNameWithPath, quiet, traceEnabled = false) + override fun launchEmulator(selectedEmulator: Int, programFile: Path, quiet: Boolean) { + launchEmulatorWithTrace(programFile, quiet, traceEnabled = false) } - fun launchEmulatorWithTrace(programNameWithPath: Path, quiet: Boolean, traceEnabled: Boolean) { + /** + * Runs the given .p8ir file in the embedded virtual machine. + * The caller must pass the exact path; no fallback probing happens here, the -vm CLI flow resolves a bare name itself. + */ + fun launchEmulatorWithTrace(programFile: Path, quiet: Boolean, traceEnabled: Boolean) { if(!quiet) println("\nStarting Virtual Machine...") // to not have external module dependencies in our own module, we launch the virtual machine via reflection val vm = Class.forName("prog8.vm.VmRunner").getDeclaredConstructor().newInstance() as IVirtualMachineRunner - val withExt = if(programNameWithPath.extension=="p8ir") programNameWithPath else programNameWithPath.resolveSibling("${programNameWithPath.name}.p8ir") - if(withExt.isReadable()) - vm.runProgram(withExt.readText(), quiet, traceEnabled) + if(programFile.isReadable()) + vm.runProgram(programFile.readText(), quiet, traceEnabled) else - throw java.nio.file.NoSuchFileException(withExt.name, null, "not a .p8ir file") + throw java.nio.file.NoSuchFileException(programFile.toString(), null, "not a .p8ir file") } override fun isIOAddress(address: UInt): Boolean = false diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt index 999aa62a7..99f117b44 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt @@ -110,7 +110,7 @@ class AsmGen6502Internal ( programGen.generate() if(errors.noErrors()) { - val output = options.outputDir.resolve("${program.name}.asm") + val output = OutputFiles.of(options, program.name).asm() val asmLines = assembly.toMutableList() if(options.compTarget.name==Cx16Target.NAME) { scanInvalid65816instructions(asmLines) @@ -130,7 +130,7 @@ class AsmGen6502Internal ( if(options.dumpVariables) dumpVariables() - return AssemblyProgram6502(program.name, options.outputDir, options.compTarget) + return AssemblyProgram6502(program.name, options, options.compTarget) } else { errors.report() return null @@ -2088,11 +2088,17 @@ $repeatLabel""") throw AssemblyError("%asmbinary inside non-library/non-filesystem module not yet supported") val sourcePath = Path(incbin.definingBlock()!!.source.origin) val includedPath = sourcePath.resolveSibling(incbin.file) - val pathForAssembler = options.outputDir // #54: 64tass needs the path *relative to the .asm file* - .sanitize() - .relativize(includedPath.sanitize()) - .normalize() // avoid assembler warnings (-Wportable; only some, not all) - .toString().replace('\\', '/') + val pathForAssembler = try { + options.outputDir // #54: 64tass needs the path *relative to the .asm file* + .sanitize() + .relativize(includedPath.sanitize()) + .normalize() // avoid assembler warnings (-Wportable; only some, not all) + .toString().replace('\\', '/') + } catch (_: IllegalArgumentException) { + // Path.relativize throws when the two paths have different roots (different drive/volume) + errors.err("cannot determine relative path for %asmbinary file '${incbin.file}' (at ${includedPath}): it must be on the same drive/volume as the generated assembly in ${options.outputDir}", incbin.position) + return + } out(" .binary \"$pathForAssembler\" $offset $length") } diff --git a/codeGenIntermediate/src/prog8/codegen/vm/VmCodeGen.kt b/codeGenIntermediate/src/prog8/codegen/vm/VmCodeGen.kt index 446e20c4f..99392b0d7 100644 --- a/codeGenIntermediate/src/prog8/codegen/vm/VmCodeGen.kt +++ b/codeGenIntermediate/src/prog8/codegen/vm/VmCodeGen.kt @@ -6,12 +6,9 @@ import prog8.code.assembly.IAssemblyProgram import prog8.code.ast.PtProgram import prog8.code.core.CompilationOptions import prog8.code.core.IErrorReporter +import prog8.code.core.OutputFiles import prog8.codegen.intermediate.IRCodeGen -import prog8.intermediate.IRDataType -import prog8.intermediate.IRFileWriter -import prog8.intermediate.IRProgram -import prog8.intermediate.VirtualRegister -import prog8.intermediate.dumpVariables +import prog8.intermediate.* class VmCodeGen(val retainSSA: Boolean, private val preassignedCallSiteIds: Map = emptyMap() @@ -54,8 +51,9 @@ internal class VmAssemblyProgram( get() = irProgram.countUsedRegisters() override fun assemble(options: CompilationOptions, errors: IErrorReporter): Boolean { - // the VM reads the IR file from disk. - IRFileWriter(irProgram, null).write() + // the VM reads the IR file from disk; for the virtual target the IR file is the program artifact. + val programFile = OutputFiles.of(options, irProgram.name).programFile + IRFileWriter(irProgram, programFile).write() return true } } diff --git a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt index 6e62cc574..9bde3f8ea 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt @@ -577,7 +577,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT } val options = program.options - val asmFile = options.outputDir.resolve("${program.name}.asm") + val asmFile = OutputFiles.of(options, program.name).asm() try { if (options.optimize) { val asmLines = output.toString().lines().toMutableList() diff --git a/codeGenM68k/src/prog8/codegen/m68k/AssemblyProgramM68k.kt b/codeGenM68k/src/prog8/codegen/m68k/AssemblyProgramM68k.kt index f621faa88..b9a765ba9 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/AssemblyProgramM68k.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/AssemblyProgramM68k.kt @@ -9,15 +9,13 @@ import java.nio.file.Files import java.nio.file.Path import kotlin.io.path.readText -class AssemblyProgramM68k(override val name: String, private val outputDir: Path) : IAssemblyProgram { +class AssemblyProgramM68k(override val name: String) : IAssemblyProgram { override val irInstructionCount: Int = 0 override val irChunkCount: Int = 0 override val irRegisterCount: Int = 0 - private val assemblyFile = outputDir.resolve("$name.asm") - - fun elfFile(): Path = outputDir.resolve("$name.elf") + private fun elfFile(outs: OutputFiles): Path = outs.programFile /** * A target supplies its own memory layout as a "link.ld" file. A target defined by a @@ -68,12 +66,13 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path else -> error("invalid cpu type for m68k codegen ${options.compTarget.cpu}") } val assemblerCpu = if(options.compTarget.name == "amiga500" && options.floats) "68020" else cpu + val outs = OutputFiles.of(options, name) when(options.output) { OutputType.ELF -> { // Step 1: assemble to ELF object file - val objFile = outputDir.resolve("$name.o") - val listFile = outputDir.resolve("$name.list") + val objFile = outs.obj() + val listFile = outs.list() val assembleCmd = mutableListOf( "vasmm68k_mot", "-m$assemblerCpu", @@ -84,7 +83,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path "-ldots", "-spaces", "-o", objFile.toString(), - assemblyFile.toString() + outs.asm().toString() ) if (options.asmListfile) assembleCmd.addAll(listOf("-L", listFile.toString())) @@ -93,11 +92,11 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path if (!runProcess(assembleCmd, options.quiet, "vasm")) return false // clean up any leftover ELF/obj files from previous builds - Files.deleteIfExists(outputDir.resolve("$name.bin")) + Files.deleteIfExists(outs.binFile()) // Step 2: write linker script and link to ELF executable - val linkScript = outputDir.resolve("$name.link.ld") - val elfFile = elfFile() + val linkScript = outs.linkScript() + val elfFile = elfFile(outs) Files.writeString(linkScript, resolveLinkerScript(options.compTarget)) val linkCmd = listOf( @@ -116,8 +115,8 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path } OutputType.AMIGAHUNK -> { // Step 1: assemble directly to AmigaHunk executable file - val exefile = outputDir.resolve(name) - val listfile = outputDir.resolve("$name.list") + val exefile = outs.programFile + val listfile = outs.list() val assembleCmd = when(options.compTarget) { is Amiga500Target -> { // amiga 500 with kickstart 1.3 @@ -133,7 +132,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path "-spaces", "-nosym", // no debug symbols "-o", exefile.toString(), - assemblyFile.toString() + outs.asm().toString() ).also { cmd -> if (options.asmListfile) { cmd.add("-L") @@ -157,7 +156,7 @@ class AssemblyProgramM68k(override val name: String, private val outputDir: Path "-spaces", "-nosym", // no debug symbols "-o", exefile.toString(), - assemblyFile.toString() + outs.asm().toString() ).also { cmd -> if (options.asmListfile) { cmd.add("-L") diff --git a/codeGenM68k/src/prog8/codegen/m68k/M68kCodeGenerator.kt b/codeGenM68k/src/prog8/codegen/m68k/M68kCodeGenerator.kt index 325305036..3c5d61b85 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/M68kCodeGenerator.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/M68kCodeGenerator.kt @@ -37,6 +37,6 @@ class M68kCodeGenerator(val retainSSA: Boolean): ICodeGeneratorBackend { if (!gen.generate()) throw RuntimeException("M68k assembly generation failed") - return AssemblyProgramM68k(irProgram.name, irProgram.options.outputDir) + return AssemblyProgramM68k(irProgram.name) } } diff --git a/codeGenM68k/src/prog8/codegen/m68k/Main.kt b/codeGenM68k/src/prog8/codegen/m68k/Main.kt index 798f0e66e..6a9442f31 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/Main.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/Main.kt @@ -4,9 +4,11 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.parameters.arguments.argument import prog8.code.core.CpuType +import prog8.code.core.OutputFiles import prog8.intermediate.IRFileReader import kotlin.io.path.Path import kotlin.io.path.readText +import kotlin.system.exitProcess fun main(args: Array) { val cli = M68kCli() @@ -23,8 +25,12 @@ fun main(args: Array) { when(target.cpu) { CpuType.M68000, CpuType.M68020 -> { val gen = AsmGen(program, target) - gen.generate() - println("Generated assembly: ${program.name}.asm") + val ok = gen.generate() + if(!ok) { + System.err.println("Assembly failed.") + exitProcess(1) + } + println("Generated assembly: ${OutputFiles.of(program.options, program.name).asm()}") } else -> { error("This code generator only works for M68000 or M68020 CPU.") diff --git a/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt b/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt index f5c368f76..eaa7652ee 100644 --- a/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt +++ b/codeGenNew6502/src/prog8/codegen/new6502/AsmGen.kt @@ -117,7 +117,7 @@ internal class AsmGen(val program: IRProgram, private val target: ICompilationTa emitBssSection() val options = program.options - val asmFile = options.outputDir.resolve("${program.name}.asm") + val asmFile = OutputFiles.of(options, program.name).asm() try { asmFile.toFile().writeText(output.toString()) } catch (e: Exception) { diff --git a/codeGenNew6502/src/prog8/codegen/new6502/New6502CodeGenerator.kt b/codeGenNew6502/src/prog8/codegen/new6502/New6502CodeGenerator.kt index a4c0cbe50..47bcc7dbc 100644 --- a/codeGenNew6502/src/prog8/codegen/new6502/New6502CodeGenerator.kt +++ b/codeGenNew6502/src/prog8/codegen/new6502/New6502CodeGenerator.kt @@ -41,6 +41,6 @@ class New6502CodeGenerator(val retainSSA: Boolean, if(!success) return null - return AssemblyProgram6502(irProgram.name, irProgram.options.outputDir, irProgram.options.compTarget) + return AssemblyProgram6502(irProgram.name, irProgram.options, irProgram.options.compTarget) } } diff --git a/compiler/src/prog8/CompilerMain.kt b/compiler/src/prog8/CompilerMain.kt index 733a00044..5b7dcc220 100644 --- a/compiler/src/prog8/CompilerMain.kt +++ b/compiler/src/prog8/CompilerMain.kt @@ -6,13 +6,11 @@ import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.core.parse import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.arguments.multiple -import com.github.ajalt.clikt.parameters.options.default -import com.github.ajalt.clikt.parameters.options.flag -import com.github.ajalt.clikt.parameters.options.multiple -import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.types.choice import com.github.ajalt.clikt.parameters.types.int import prog8.ast.AstException +import prog8.code.core.OutputFiles import prog8.code.source.ImportFileSystem import prog8.code.source.ImportFileSystem.expandTilde import prog8.code.target.CompilationTargets @@ -88,7 +86,9 @@ private class CompilerCli : CliktCommand(name = "prog8c") { val profilingInstrumentation by option("-profiling", "--profiling", help = "add subroutine profiling instrumentation (cx16 only)").flag() val dontWriteAssembly by option("-noasm", "--noasm", help = "don't create assembly code").flag() val dontOptimize by option("-noopt", "--noopt", help = "don't perform code optimizations").flag() - val outputDir by option("-out", "--out", help = "directory for output files instead of current directory").default(".") + val outputFile by option("-o", "--output", help = "name of the output program file (and directory for the other output files)") + val outputDir by option("-out", "--out", help = "directory for output files instead of current directory") + .transformAll(defaultForHelp = ".") { ValueWithDefault(it.lastOrNull(), ".") } val plainText by option("-plaintext", "--plaintext", help = "output only plain text, no colors or fancy symbols").flag() val printAst1 by option("-printast1", "--printast1", help = "print out the internal compiler AST").flag() val printAst2 by option("-printast2", "--printast2", help = "print out the simplified AST that is used for code generation").flag() @@ -148,8 +148,19 @@ private fun compileMain(args: Array): Boolean { return true } - val outputPath = pathFrom(outputDir) - outputPath.createDirectories() + if(outputFile!=null && outputDir.explicit!=null) { + presenter.printErrorLine("Use either -o (output file) or -out (output directory), not both.") + return false + } + if(outputFile!=null && moduleFiles.size>1) { + presenter.printErrorLine("The -o option can only be used with a single module file.") + return false + } + val outputFilePath = outputFile?.let { pathFrom(it) } + if(outputFilePath!=null && outputFilePath.isDirectory()) { + presenter.printErrorLine("The -o option must name a file, not a directory: $outputFile") + return false + } if(profilingInstrumentation==true && compilationTarget!=Cx16Target.NAME) { presenter.printErrorLine("Profiling instrumentation is only available on the cx16 target.") @@ -219,12 +230,15 @@ private fun compileMain(args: Array): Boolean { // CpuType and pointer size, which also covers custom targets defined by a config file. if(startVm==true) { - runVm(moduleFiles.first(), quietAll==true, vmTrace==true) - return true + return runVm(moduleFiles.first(), quietAll==true, vmTrace==true, presenter) } val processedSymbols = processSymbolDefs(symbolDefs, presenter) ?: return false + val outputPath = pathFrom(outputDir.value) + outputFile?.let { pathFrom(it).parent?.createDirectories() } + outputPath.createDirectories() + if(watchMode==true) { val watchservice = FileSystems.getDefault().newWatchService() val allImportedFiles = mutableSetOf() @@ -264,6 +278,7 @@ private fun compileMain(args: Array): Boolean { processedSymbols, srcdirs, outputPath, + outputFile = outputFile?.let { pathFrom(it) }, errors = ErrorReporter(txtcolors), assemble = true, generateDocumentation = generateDocumentation == true @@ -352,6 +367,7 @@ private fun compileMain(args: Array): Boolean { processedSymbols, absoluteSrcDirs, outputPath, + outputFile = outputFile?.let { pathFrom(it) }, cwd = Path.of(System.getProperty("user.dir")), errors = ErrorReporter(txtcolors), assemble = true, @@ -365,18 +381,17 @@ private fun compileMain(args: Array): Boolean { if (response.outputFiles.isEmpty()) { println("\nCan't start emulator because no program was assembled.") } else { - val programPathRaw = response.outputFiles.first() - val programPath = Path.of(programPathRaw.removeSuffix(".prg")) - + val programFile = Path.of(response.outputFiles.first()) + val target = getCompilationTargetByName(compilationTarget!!) if (startEmulator1 == true) { if (target is VMTarget) { - target.launchEmulatorWithTrace(programPath, quietAll==true, vmTrace==true) + target.launchEmulatorWithTrace(programFile, quietAll==true, vmTrace==true) } else { - target.launchEmulator(1, programPath, quietAll==true) + target.launchEmulator(1, programFile, quietAll==true) } } else { - target.launchEmulator(2, programPath, quietAll==true) + target.launchEmulator(2, programFile, quietAll==true) } } } @@ -420,6 +435,7 @@ private fun compileMain(args: Array): Boolean { processedSymbols, srcdirs, outputPath, + outputFile = outputFile?.let { pathFrom(it) }, errors = ErrorReporter(txtcolors), assemble = true, generateDocumentation = generateDocumentation == true @@ -444,22 +460,21 @@ private fun compileMain(args: Array): Boolean { } } - val programNameInPath = outputPath.resolve(compilationResult.compilerAst.name) - val programPath = Path.of(programNameInPath.toString().removeSuffix(".prg")) + val outs = OutputFiles.of(compilationResult.compilationOptions, compilationResult.compilerAst.name) if (compareIR != null) { - compareIrFiles(outputPath.resolve("${compilationResult.compilerAst.name}.p8ir"), Path(compareIR!!), presenter) + compareIrFiles(outs.ir(), Path(compareIR!!), presenter) } if (startEmulator1 == true) { if (compilationResult.compilationOptions.compTarget is VMTarget) { (compilationResult.compilationOptions.compTarget as VMTarget).launchEmulatorWithTrace( - programPath, quietAll==true, vmTrace==true) + outs.programFile, quietAll==true, vmTrace==true) } else { - compilationResult.compilationOptions.compTarget.launchEmulator(1, programPath, quietAll==true) + compilationResult.compilationOptions.compTarget.launchEmulator(1, outs.programFile, quietAll==true) } } else if (startEmulator2 == true) - compilationResult.compilationOptions.compTarget.launchEmulator(2, programPath, quietAll==true) + compilationResult.compilationOptions.compTarget.launchEmulator(2, outs.programFile, quietAll==true) } } @@ -507,10 +522,18 @@ private fun processSymbolDefs(symbolDefs: List, presenter: Presenter): M return result } -fun runVm(irFilename: String, quiet: Boolean, traceEnabled: Boolean = false) { +private fun runVm(irFilename: String, quiet: Boolean, traceEnabled: Boolean, presenter: Presenter): Boolean { val irFile = Path(irFilename) + // -vm accepts a bare module name, so append the .p8ir extension here; this fallback must not + // live in launchEmulatorWithTrace, where an extension-less -o would otherwise be mangled + val p8irFile = if(irFile.extension=="p8ir") irFile else irFile.resolveSibling("${irFile.name}.p8ir") + if(!p8irFile.isReadable()) { + presenter.printErrorLine("Can't run the virtual machine: no such file: $p8irFile") + return false + } val vmdef = VMTarget() - vmdef.launchEmulatorWithTrace(irFile, quiet, traceEnabled) + vmdef.launchEmulatorWithTrace(p8irFile, quiet, traceEnabled) + return true } private fun compareIrFiles(newFile: Path, baselineFile: Path, presenter: Presenter) { @@ -865,6 +888,7 @@ private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: Compiler symbolDefs = compilerArgs.symbolDefs, sourceDirs = compilerArgs.sourceDirs, outputDir = compilerArgs.outputDir.toString(), + outputFile = compilerArgs.outputFile?.toString(), cwd = compilerArgs.cwd.toString(), generateDocumentation = compilerArgs.generateDocumentation ) diff --git a/compiler/src/prog8/compiler/Compiler.kt b/compiler/src/prog8/compiler/Compiler.kt index eeeef034d..79ec4f7dc 100644 --- a/compiler/src/prog8/compiler/Compiler.kt +++ b/compiler/src/prog8/compiler/Compiler.kt @@ -68,6 +68,7 @@ class CompilerArguments(val filepath: Path, val symbolDefs: Map, val sourceDirs: List = emptyList(), val outputDir: Path = Path(""), + val outputFile: Path? = null, val cwd: Path = Path("").absolute(), val errors: IErrorReporter = ErrorReporter(ErrorReporter.AnsiColors), val assemble: Boolean = true, @@ -144,7 +145,9 @@ fun compileProgram(args: CompilerArguments): CompilationResult? { varsGolden = args.varsGolden if(args.varsAddress!=null) varsAddress = args.varsAddress - outputDir = args.outputDir.normalize() + outputDir = args.outputFile?.parent ?: args.outputDir.normalize() + // the .asm and outputDir must move together: 64tass resolves .binary relative to the .asm + outputFile = args.outputFile symbolDefs = args.symbolDefs } // apply custom target default for vars address now so it participates in the ROMable check diff --git a/compiler/src/prog8/compiler/CompilerDaemon.kt b/compiler/src/prog8/compiler/CompilerDaemon.kt index 33463c41d..c330f9e9a 100644 --- a/compiler/src/prog8/compiler/CompilerDaemon.kt +++ b/compiler/src/prog8/compiler/CompilerDaemon.kt @@ -2,8 +2,8 @@ package prog8.compiler import prog8.buildversion.BUILD_UNIX_TIME import prog8.code.core.IErrorReporter +import prog8.code.core.OutputFiles import prog8.code.source.ImportFileSystem -import prog8.code.target.VMTarget import java.io.* import java.net.StandardProtocolFamily import java.net.UnixDomainSocketAddress @@ -144,19 +144,21 @@ internal class CompilerDaemon(private val socketPath: Path) { System.setErr(oldErr) } - val outputFiles = if (result != null) + // list produced output files, program artifact first; the client uses the first entry to feed an emulator + val outputFiles = if (result != null && request.writeAssembly) { + val outs = OutputFiles.of(result.compilationOptions, result.compilerAst.name) listOfNotNull( - request.outputDir.let { d -> - val name = Path.of(request.filepath).fileName.toString().substringBeforeLast('.') - val dir = if (d == ".") "" else "$d/" - listOf( - "${dir}${name}.prg".takeIf { request.writeAssembly && request.compilationTarget != VMTarget.NAME }, - "${dir}${name}.asm".takeIf { request.writeAssembly && request.compilationTarget != VMTarget.NAME }, - "${dir}${name}.p8ir".takeIf { !request.writeAssembly || request.compilationTarget == VMTarget.NAME } - ).filterNotNull() - } - ).flatten() - else + outs.programFile.takeIf { Files.isRegularFile(it) }?.toString(), + outs.asm().takeIf { Files.isRegularFile(it) }?.toString(), + outs.list().takeIf { Files.isRegularFile(it) }?.toString(), + outs.viceMonList().takeIf { Files.isRegularFile(it) }?.toString(), + outs.binFile().takeIf { Files.isRegularFile(it) }?.toString(), + outs.ir().takeIf { Files.isRegularFile(it) }?.toString(), + outs.bankedCalls().takeIf { Files.isRegularFile(it) }?.toString(), + outs.obj().takeIf { Files.isRegularFile(it) }?.toString(), + outs.linkScript().takeIf { Files.isRegularFile(it) }?.toString() + ).distinct() + } else emptyList() val importedFiles = result?.importedFiles?.map { it.toString() } ?: emptyList() @@ -192,6 +194,10 @@ internal class CompilerDaemon(private val socketPath: Path) { } else { clientCwd.resolve(outputDir) } + val resolvedOutputFile = outputFile?.let { + val p = Path.of(it) + if (p.isAbsolute) p else clientCwd.resolve(p) + } return CompilerArguments( filepath = Path.of(filepath), optimize = optimize, @@ -220,6 +226,7 @@ internal class CompilerDaemon(private val socketPath: Path) { symbolDefs = symbolDefs, sourceDirs = sourceDirs, outputDir = resolvedOutputDir, + outputFile = resolvedOutputFile, cwd = clientCwd, errors = errors, generateDocumentation = generateDocumentation diff --git a/compiler/src/prog8/compiler/DaemonProtocol.kt b/compiler/src/prog8/compiler/DaemonProtocol.kt index 53ca6d644..09f54dcec 100644 --- a/compiler/src/prog8/compiler/DaemonProtocol.kt +++ b/compiler/src/prog8/compiler/DaemonProtocol.kt @@ -30,6 +30,7 @@ internal data class DaemonRequest( val symbolDefs: Map, val sourceDirs: List, val outputDir: String, + val outputFile: String? = null, val cwd: String, val generateDocumentation: Boolean = false ) @@ -78,6 +79,7 @@ internal object DaemonProtocol { append(prop("symbolDefs", req.symbolDefs)) append(prop("sourceDirs", req.sourceDirs)) append(prop("outputDir", req.outputDir)) + append(propOpt("outputFile", req.outputFile)) append(prop("cwd", req.cwd)) append(prop("generateDocumentation", req.generateDocumentation)) append("null") // placeholder, gets overwritten by trimEnd @@ -145,6 +147,7 @@ internal object DaemonProtocol { symbolDefs = (map["symbolDefs"] as? Map<*, *>)?.mapKeys { it.key as String }?.mapValues { it.value as String } ?: emptyMap(), sourceDirs = (map["sourceDirs"] as? List<*>)?.map { it as String } ?: emptyList(), outputDir = map["outputDir"] as String, + outputFile = map["outputFile"] as? String, cwd = map["cwd"] as String, generateDocumentation = map["generateDocumentation"] as? Boolean ?: false ) diff --git a/compiler/test/TestCompilerOnExamples.kt b/compiler/test/TestCompilerOnExamples.kt index 5755915e2..ecb82bf10 100644 --- a/compiler/test/TestCompilerOnExamples.kt +++ b/compiler/test/TestCompilerOnExamples.kt @@ -6,6 +6,7 @@ import io.kotest.engine.spec.tempdir import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import prog8.code.core.ICompilationTarget +import prog8.code.core.OutputFiles import prog8.code.target.* import prog8.compiler.CompilationResult import prog8.compiler.CompilerArguments @@ -107,10 +108,7 @@ private fun prepareTestFiles(source: String, optimize: Boolean, target: ICompila * A small tolerance of ±5 bytes is allowed for minor non-deterministic variations. */ private fun verifyOutputFileSize(result: CompilationResult, expectedSize: Int) { - val outputFile = when (result.compilationOptions.compTarget) { - is VMTarget -> result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir") - else -> result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".prg") - } + val outputFile = OutputFiles.of(result.compilationOptions, result.compilerAst.name).programFile val actualSize = Files.size(outputFile).toInt() val tolerance = 5 // Allow ±5 bytes for minor non-deterministic variations if (actualSize < expectedSize - tolerance || actualSize > expectedSize + tolerance) { diff --git a/compiler/test/TestDaemonProtocol.kt b/compiler/test/TestDaemonProtocol.kt index 6406a330e..ea7eefec3 100644 --- a/compiler/test/TestDaemonProtocol.kt +++ b/compiler/test/TestDaemonProtocol.kt @@ -38,6 +38,7 @@ class TestDaemonProtocol : FunSpec({ symbolDefs = mapOf("FOO" to "123", "BAR" to "456"), sourceDirs = listOf("/src1", "/src2"), outputDir = "/out", + outputFile = "/out/custom.pgz", cwd = "/home/user/project" ) @@ -71,6 +72,7 @@ class TestDaemonProtocol : FunSpec({ decoded.symbolDefs shouldBe request.symbolDefs decoded.sourceDirs shouldBe request.sourceDirs decoded.outputDir shouldBe request.outputDir + decoded.outputFile shouldBe request.outputFile decoded.cwd shouldBe request.cwd } @@ -114,6 +116,7 @@ class TestDaemonProtocol : FunSpec({ decoded.varsAddress shouldBe null decoded.breakpointCpuInstruction shouldBe null decoded.newCodegen shouldBe false + decoded.outputFile shouldBe null } test("Decode fails gracefully with missing field") { diff --git a/compiler/test/TestOutputFileName.kt b/compiler/test/TestOutputFileName.kt new file mode 100644 index 000000000..5857fe467 --- /dev/null +++ b/compiler/test/TestOutputFileName.kt @@ -0,0 +1,288 @@ +package prog8tests.compiler + +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.FunSpec +import io.kotest.engine.spec.tempdir +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import prog8.code.core.* +import prog8.code.target.C64Target +import prog8.code.target.Cx16Target +import prog8.code.target.VMTarget +import prog8.compiler.CompilationResult +import prog8.compiler.CompilerArguments +import prog8.compiler.compileProgram +import prog8tests.helpers.ErrorReporterForTests +import prog8tests.helpers.assumeReadableFile +import prog8tests.helpers.fixturesDir +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.* + + +private val minimalProgram = """ + %zeropage basicsafe + %option no_sysinit + main { + sub start() { + cx16.r0++ + } + } +""" + +private fun writeSourceTo(dir: Path, source: String): Path { + val filepath = dir.resolve("testoutput.p8") + filepath.toFile().writeText(source) + return filepath +} + +// local copy of the compileTheThing pattern: the shared compileText/compileFile helpers cannot pass an outputFile +private fun compileWithOutputFile( + filepath: Path, + target: ICompilationTarget, + outputDir: Path, + outputFile: Path, + assemble: Boolean = false +): CompilationResult? { + val args = CompilerArguments( + filepath, + optimize = false, + writeAssembly = true, + warnSymbolShadowing = false, + warnImplicitTypeCasts = false, + quietAll = true, + quietAssembler = true, + showTimings = false, + asmListfile = false, + includeSourcelines = false, + newCodegen = false, + dumpVariables = false, + dumpSymbols = false, + varsHighBank = null, + varsGolden = false, + varsAddress = null, + compilationTarget = target.name, + breakpointCpuInstruction = null, + printAst1 = false, + printAst2 = false, + ignoreFootguns = false, + profilingInstrumentation = false, + symbolDefs = emptyMap(), + outputDir = outputDir, + outputFile = outputFile, + errors = ErrorReporterForTests(), + assemble = assemble + ) + return compileProgram(args) +} + +class TestOutputFileName : FunSpec({ + + test("outputStemOf strips the last extension") { + outputStemOf(Path.of("game.pgz")) shouldBe "game" + outputStemOf(Path.of("my.game")) shouldBe "my" + } + + test("outputStemOf keeps names without an extension unchanged") { + outputStemOf(Path.of("game")) shouldBe "game" + } + + test("outputStemOf treats a leading dot as not an extension") { + outputStemOf(Path.of(".pgz")) shouldBe ".pgz" + } + + test("programFile maps each OutputType to its legacy extension") { + val outputDir = tempdir().toPath() + fun options(output: OutputType) = CompilationOptions.builder(C64Target()) + .output(output) + .outputDir(outputDir) + .build() + options(OutputType.PRG).programFile("prog") shouldBe outputDir.resolve("prog.prg") + options(OutputType.XEX).programFile("prog") shouldBe outputDir.resolve("prog.xex") + options(OutputType.RAW).programFile("prog") shouldBe outputDir.resolve("prog.bin") + options(OutputType.LIBRARY).programFile("prog") shouldBe outputDir.resolve("prog.bin") + options(OutputType.ELF).programFile("prog") shouldBe outputDir.resolve("prog.elf") + options(OutputType.AMIGAHUNK).programFile("prog") shouldBe outputDir.resolve("prog") + } + + test("programFile special-cases VMTarget to the p8ir artifact") { + val outputDir = tempdir().toPath() + val options = CompilationOptions.builder(VMTarget()) + .output(OutputType.ELF) + .outputDir(outputDir) + .build() + options.programFile("prog") shouldBe outputDir.resolve("prog.p8ir") + } + + test("programFile enforces the .p8ir extension for -o on the virtual target") { + val tmp = tempdir().toPath() + val builder = { ir: Path -> + CompilationOptions.builder(VMTarget()).outputDir(tmp).outputFile(ir).build() + } + // the VM identifies the IR by its extension, so an extension-less -o must get one appended + builder(tmp.resolve("game")).programFile("prog") shouldBe tmp.resolve("game.p8ir") + builder(tmp.resolve("game.p8ir")).programFile("prog") shouldBe tmp.resolve("game.p8ir") + builder(tmp.resolve("game.P8IR")).programFile("prog") shouldBe tmp.resolve("game.P8IR") + builder(tmp.resolve("sub/game")).programFile("prog") shouldBe tmp.resolve("sub/game.p8ir") + + // ... and the stem must follow the resolved name, so the aux files stay consistent with the artifact + OutputFiles.of(builder(tmp.resolve("game")), "prog").ir() shouldBe tmp.resolve("game.p8ir") + OutputFiles.of(builder(tmp.resolve("game")), "prog").stem shouldBe "game" + } + + test("OutputFiles.of with -o resolves the artifact verbatim and all aux files from its stem") { + val tmp = tempdir().toPath() + val outDir = tmp.resolve("out") + val options = CompilationOptions.builder(Cx16Target()) + .outputDir(tmp) + .outputFile(outDir.resolve("custom.pgz")) + .build() + val outs = OutputFiles.of(options, "prog") + outs.programFile shouldBe outDir.resolve("custom.pgz") + outs.stem shouldBe "custom" + outs.dir shouldBe outDir + outs.asm() shouldBe outDir.resolve("custom.asm") + outs.list() shouldBe outDir.resolve("custom.list") + outs.viceMonList() shouldBe outDir.resolve("custom.vice-mon-list") + outs.binFile() shouldBe outDir.resolve("custom.bin") + outs.ir() shouldBe outDir.resolve("custom.p8ir") + outs.bankedCalls() shouldBe outDir.resolve("custom.bankedcalls") + outs.obj() shouldBe outDir.resolve("custom.o") + outs.linkScript() shouldBe outDir.resolve("custom.link.ld") + } + + test("viceMonList is derived from the stem, never from the program file name") { + val tmp = tempdir().toPath() + val outDir = tmp.resolve("out") + val options = CompilationOptions.builder(Cx16Target()) + .outputDir(tmp) + .outputFile(outDir.resolve("custom.pgz")) + .build() + val outs = OutputFiles.of(options, "prog") + withClue("vice-mon-list must not keep the program file extension") { + outs.viceMonList() shouldNotBe outDir.resolve("custom.pgz.vice-mon-list") + } + } + + test("OutputFiles.of with a bare -o filename resolves aux files relative to outputDir") { + val tmp = tempdir().toPath() + val options = CompilationOptions.builder(Cx16Target()) + .outputDir(tmp) + .outputFile(Path.of("game.pgz")) + .build() + val outs = OutputFiles.of(options, "prog") + outs.programFile shouldBe Path.of("game.pgz") + outs.dir shouldBe tmp + outs.asm() shouldBe tmp.resolve("game.asm") + outs.ir() shouldBe tmp.resolve("game.p8ir") + } + + test("legacy naming keeps the full dotted program stem when -o is not used") { + val tmp = tempdir().toPath() + val options = CompilationOptions.builder(Cx16Target()).outputDir(tmp).build() + val outs = OutputFiles.of(options, "my.game") + withClue("legacy stem must be used verbatim, not re-derived by outputStemOf") { + outs.stem shouldBe "my.game" + outs.asm() shouldBe tmp.resolve("my.game.asm") + outs.list() shouldBe tmp.resolve("my.game.list") + outs.viceMonList() shouldBe tmp.resolve("my.game.vice-mon-list") + outs.ir() shouldBe tmp.resolve("my.game.p8ir") + outs.bankedCalls() shouldBe tmp.resolve("my.game.bankedcalls") + outs.obj() shouldBe tmp.resolve("my.game.o") + outs.linkScript() shouldBe tmp.resolve("my.game.link.ld") + } + outs.asm() shouldNotBe tmp.resolve("my.asm") + } + + test("legacy program file path is unchanged without -o") { + val tmp = tempdir().toPath() + val options = CompilationOptions.builder(Cx16Target()).outputDir(tmp).build() + OutputFiles.of(options, "prog").programFile shouldBe tmp.resolve("prog.prg") + } + + test("compiling with -o writes the aux files next to the artifact") { + val outputDir = tempdir().toPath() + val outDir = outputDir.resolve("out") + outDir.createDirectories() + val outputFile = outDir.resolve("custom.pgz") + val filepath = writeSourceTo(outputDir, minimalProgram) + val result = compileWithOutputFile(filepath, Cx16Target(), outputDir, outputFile) + result shouldNotBe null + withClue("no assembler ran, so the artifact must not exist yet") { + outputFile.exists() shouldBe false + } + withClue("the aux .asm must be named after the -o stem") { + outDir.resolve("custom.asm").exists() shouldBe true + } + withClue("no source-named .asm may appear in the output directory") { + outDir.resolve("testoutput.asm").exists() shouldBe false + val topLevelAsms = Files.list(outputDir).use { stream -> + stream.filter { it.name.endsWith(".asm") }.toList() + } + topLevelAsms shouldBe emptyList() + } + result!!.compilationOptions.outputFile shouldBe outputFile + } + + test("compiling with -o and assemble=true writes the final artifact to the -o path") { + val outputDir = tempdir().toPath() + val outDir = outputDir.resolve("out") + outDir.createDirectories() + val outputFile = outDir.resolve("custom.prg") + val filepath = writeSourceTo(outputDir, minimalProgram) + val result = compileWithOutputFile(filepath, Cx16Target(), outputDir, outputFile, assemble = true) + result shouldNotBe null + withClue("the assembled artifact must exist at the -o path") { + outputFile.exists() shouldBe true + outputFile.isRegularFile() shouldBe true + } + withClue("the aux .asm must be named after the -o stem") { + outDir.resolve("custom.asm").exists() shouldBe true + } + } + + test("%asmbinary under -o keeps the .binary path relative to the generated .asm") { + val outputDir = tempdir().toPath() + val outputFile = outputDir.resolve("game.prg") + val p8Path = assumeReadableFile(fixturesDir, "asmBinaryFromSameFolder.p8") + val result = compileWithOutputFile(p8Path, Cx16Target(), outputDir, outputFile) + result shouldNotBe null + val asmPath = OutputFiles.of(result!!.compilationOptions, result.compilerAst.name).asm() + asmPath.exists() shouldBe true + val asmLines = asmPath.readLines().map { it.trim() } + val binaryLine = asmLines.first { it.contains(".binary") } + val binPath = binaryLine.substringAfter(".binary \"").substringBefore('"') + withClue("the .binary path must be relative to the .asm file, not absolute: $binaryLine") { + Path.of(binPath).isAbsolute shouldBe false + } + } + + test("virtual target -o writes the .p8ir artifact") { + val outputDir = tempdir().toPath() + val outputFile = outputDir.resolve("ir.p8ir") + val filepath = writeSourceTo(outputDir, minimalProgram) + val result = compileWithOutputFile(filepath, VMTarget(), outputDir, outputFile, assemble = true) + result shouldNotBe null + outputFile.exists() shouldBe true + outputFile.isRegularFile() shouldBe true + } + + test("virtual target -o without extension still writes a .p8ir artifact") { + val outputDir = tempdir().toPath() + val filepath = writeSourceTo(outputDir, minimalProgram) + val result = compileWithOutputFile(filepath, VMTarget(), outputDir, outputDir.resolve("ir"), assemble = true) + result shouldNotBe null + outputDir.resolve("ir.p8ir").exists() shouldBe true + outputDir.resolve("ir").exists() shouldBe false + } + + test("virtual target -o preserves an uppercase .P8IR extension") { + val outputDir = tempdir().toPath() + val outputFile = outputDir.resolve("game.P8IR") + val filepath = writeSourceTo(outputDir, minimalProgram) + val result = compileWithOutputFile(filepath, VMTarget(), outputDir, outputFile, assemble = true) + result shouldNotBe null + outputFile.exists() shouldBe true + outputDir.resolve("game.p8ir").exists() shouldBe false + } +}) diff --git a/docs/CODEBASE-KNOWLEDGE-GRAPH.md b/docs/CODEBASE-KNOWLEDGE-GRAPH.md index cfe8304d9..d5127ceca 100644 --- a/docs/CODEBASE-KNOWLEDGE-GRAPH.md +++ b/docs/CODEBASE-KNOWLEDGE-GRAPH.md @@ -113,7 +113,7 @@ CompilerMain.compileMain() m68k -> M68kCodeGenerator vm -> VmCodeGen backend.generate(program, symbolTable, options, errors) -> IAssemblyProgram - assembly.assemble() output .prg/.asm/.p8ir + assembly.assemble() output .prg/.asm/.p8ir; program artifact name/path overridable with -o ``` **Target selection:** `compiler/src/prog8/CodeCore .../Compiler.kt:769-780`. The target CPU determines the backend. The custom target config file path is resolved in `codeCore` targets (`ConfigFileTarget.fromConfigFile`). diff --git a/docs/source/binlibrary.rst b/docs/source/binlibrary.rst index 3e938533f..98c783a65 100644 --- a/docs/source/binlibrary.rst +++ b/docs/source/binlibrary.rst @@ -70,7 +70,8 @@ source code such as %launcher, %zeropage and so on. But there is a single direct (and makes sure there won't be any initialization code left at all): ``%output library`` Together with ``%address`` and possibly ``%memtop`` -to tell the compiler what the load address of the library should be- -it will create a "library.bin" file that fulfills the requirements of a loadable binary library program as listed above. +it will create a binary output file named after the program (``.bin``) that fulfills the requirements of a loadable binary library program as listed above. +(The exact file name and location can be overridden with the ``-o`` command line option.) For older CBM targets (C64, C128 and PET) the library file *will* have a load address header, because these targets require a header to easily load files. For the other targets such as the Commander X16, diff --git a/docs/source/compiling.rst b/docs/source/compiling.rst index c5779806d..ee141e9bd 100644 --- a/docs/source/compiling.rst +++ b/docs/source/compiling.rst @@ -150,9 +150,11 @@ For normal use the compiler can be invoked with the command: tell it to compile a program for the Commander X16) -By default, assembly code is generated and written to ``sourcefile.asm``. +By default, assembly code is generated and written as ``sourcefile.asm`` in the current working directory +(the default output directory; the ``-out`` option selects a different location). It is then (automatically) fed to the `64tass `_ assembler tool that creates the final runnable program. +For the ``virtual`` target no assembly code is generated: instead an IR program file named ``sourcefile.p8ir`` is written. Command line options @@ -161,7 +163,8 @@ Command line options .. index:: pair: Usage; Command line options Options accept either a single dash or a double dash. For example, ``-target c64`` -and ``--target c64`` are equivalent. +and ``--target c64`` are equivalent. Some options have both a short and a long form +(such as ``-o`` and ``--output``); either spelling can be used. One or more .p8 module files Specify the main module file(s) to compile. @@ -251,8 +254,24 @@ One or more .p8 module files Don't perform any code optimizations. Useful for debugging or faster compilation cycles. +``-o `` + Use the exact given file name and path for the final program artifact. + The name is used verbatim: the compiler does not add or check an extension, + and ``%output`` (or the compilation target's output type) still decides the content format of the file. + All other output files (assembly, listing, vice monitor list, IR, etc.) are written in the same + directory and use the stem of this file name: for example ``-o build/game.pgz`` also produces + ``build/game.asm`` and, with ``-asmlist``, ``build/game.list``. + A file name without an extension is allowed as well. + On the ``virtual`` target the program artifact is the IR file, so ``.p8ir`` is always enforced there: + ``-o build/game`` writes ``build/game.p8ir``. + This option cannot be combined with ``-out``, and it only works when a single source file is compiled. + It is silently ignored when no program artifact is produced anyway: with ``-noasm``, ``-check``, + ``-dumpsymbols`` or ``-gendoc``. + Without this option, output file naming and placement are unchanged. + ``-out `` Sets directory location for output files instead of current directory. Creates it if it doesn't exist yet. + Cannot be combined with ``-o``. ``-plaintext`` Prints output messages in plain text: no colors or fancy symbols. @@ -433,13 +452,15 @@ The ``%breakpoint`` directive (see :ref:`directives`) in the source code instruc a *breakpoint* at that position. Some systems use a BRK instruction for this, but this will usually halt the machine altogether instead of just suspending execution. Prog8 issues a NOP instruction instead and creates a 'virtual' breakpoint at this position. -All breakpoints are then written to a file called "programname.vice-mon-list", +All breakpoints are then written to a file called ``.vice-mon-list``, which is meant to be used by the VICE and Box16 emulators. +The stem is the name of the output program file (without extension); the ``-o`` option changes it +to the stem of the given file name. It contains a series of commands for VICE's monitor, including source labels and the breakpoint settings. If you use the emulator autostart feature of the compiler, it will take care of this for you. If you launch VICE manually, you'll have to use a command line option to load this file: - ``$ x64 -moncommands programname.vice-mon-list`` + ``$ x64 -moncommands .vice-mon-list`` VICE will then use the label names in memory disassembly, and will activate any breakpoints as well. If your running program hits one of the breakpoints, VICE will halt execution and drop you into the monitor. diff --git a/docs/source/history.rst b/docs/source/history.rst index bb26661d0..5097e6c64 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -275,6 +275,9 @@ Major breaking changes that require code modifications when upgrading: - **``defer`` is now program-wide** — deferred blocks are also unwound when the program exits, or when ``return``/``exit()`` is used from the ``start()`` subroutine. Very useful to let programs cleanly return to the OS without forgetting to free resources. - **Union types** — ``union Name { ... }`` declarations where every field shares offset 0 and storage size equals the largest field; only zero-initialization (``[]``) is allowed, values must be written at runtime through typed pointers. - added ``arena`` memory allocation library + - **``-o`` / ``--output`` compiler option** - choose the exact file name and path of the output program file; + all auxiliary output files (assembly, listing, monitor list, IR) are placed alongside it. + Mutually exclusive with ``-out``. *This document summarizes major and minor releases. Bugfix releases (e.g., v12.0.1, v12.1.1) are omitted for brevity.* diff --git a/docs/source/technical.rst b/docs/source/technical.rst index bdda2266b..ef4cbd9b8 100644 --- a/docs/source/technical.rst +++ b/docs/source/technical.rst @@ -108,7 +108,8 @@ result in the *same* ID being passed to the banking routine, as the ID is unique Viewing assigned IDs During compilation, the compiler outputs a list of all assigned call-site IDs - into a file named ``.bankedcalls`` in the output directory. + into a file named ``.bankedcalls`` in the output directory + (the stem is the name of the program, or of the file given with the ``-o`` option). This information is useful for synchronizing your overlay manager with the subroutines it needs to handle. diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 4b9bbdd47..9f8aaa510 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -13,7 +13,6 @@ Future Things and Ideas - support typed pointer arrays as struct fields, curretly requires untyped pointers arrays. - allow aliasing a whole enum (``alias myEnum = MyEnum``) so that ``myEnum::Member`` works just like ``MyEnum::Member``; currently only aliasing individual enum members is supported. See ideas/alias-enums.md for the plan. - evaluated the feature suggestions in ideas/c16-banked-data.md (cx16 banked data): evaluated Sep 2026, the plan is not worth implementing as written. Banked data already works via ``-varshigh `` / ``%varsaddress`` + ``cx16.rambank()``, and the ``cx16.fetch``/``cx16.stash`` kernal primitives turn out to be neither IRQ-safe nor reentrant, and ~5x slower than a bank switch. Cheapest useful follow-up is documentation plus small ``asmsub`` wrappers in ``syslib.p8``; see the Recommendation section in that file. -- add a ``-o `` option for choosing the exact program artifact path and placing auxiliary output files alongside it; see ``ideas/outputfile.md`` for the implementation plan. Won't do's or deferred ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/ideas/outputfile.md b/ideas/outputfile.md deleted file mode 100644 index f165372bf..000000000 --- a/ideas/outputfile.md +++ /dev/null @@ -1,348 +0,0 @@ -# Plan: `-o ` option - -Implements gillham's request from [GitHub issue #238](https://github.com/irmen/prog8/issues/238): -a compiler option to specify the exact name/path of the final program artifact, with all -other output files placed alongside it. - -Tracked in `docs/source/todo.rst:16`. - -## Decided semantics - -| Case | Behaviour | -|---|---| -| `-o build/game.pgz` on a `%output PRG` target | final artifact is exactly `build/game.pgz` (verbatim, no extension check, no warning) | -| `build/game.asm`, `.list`, `.vice-mon-list`, `.p8ir`, `.bankedcalls`, `.o`, `.link.ld` | same directory `build/`, same stem `game` | -| `-o` + `-out dir` | hard error, message telling the user to pick one | -| `-o` + more than one `.p8` positional arg | hard error | -| `-o` on `virtual` target | `-o` names the `.p8ir` file itself (that *is* the program artifact there) | -| `-o` with no extension (`-o game`) | file is literally `game`; aux files are `game.asm` etc. | -| `-o` + `-noasm` / `-check` / `-dumpsymbols` / `-gendoc` | no artifact is produced at all; `-o` is silently ignored, as today | -| no `-o` | byte-for-byte identical to today (all defaults are `null`); the **legacy stem is used verbatim**, never re-derived by `outputStemOf` | - -The `-o` name is verbatim: `%output` / `OutputType` still decides the *content* format -(`--cbm-prg`, `--atari-xex`, `--nostart`, `elf32m68k`, ...), only the file name changes. -A `.pgz` name on a `%output RAW` target therefore yields a *headerless* file - the user -asked for that name, the compiler does exactly that. - -## Core mechanism - -1. **`CompilationOptions.outputFile: Path? = null`** (`codeCore/src/prog8/code/core/CompilationOptions.kt:35` area) - - the user's exact artifact path; `null` = legacy naming. - - Add it to the primary constructor **only**. `Builder.build()` - (`CompilationOptions.kt:111-119`) is a positional call that already omits the - defaulted `privateSymbols: Boolean = false` (`:39`), so a defaulted parameter does - **not** have to be added there. Do not touch `build()` - inserting a positional - argument would break the `privateSymbols` omission. - - There is no `with` block in this file; the builder is a `Builder` class with - `= apply { ... }` setters (e.g. `:105`). - - Follow the `privateSymbols` precedent for setting it after `build()`: - `.build().apply { this.outputFile = ... }` (see `compiler/src/prog8/compiler/Compiler.kt:609`). - - `determineCompilationOptions` (`compiler/src/prog8/compiler/Compiler.kt:527`) does not - set it - it defaults to `null`. - -2. **Propagate from args to options** in the `with(compilationOptions)` block - (`Compiler.kt:129-149`, `outputDir` is assigned at `:147`): - ```kotlin - outputDir = args.outputFile?.parent ?: args.outputDir.normalize() - outputFile = args.outputFile - ``` - - Re-pointing `outputDir` to the `-o` parent makes the `.binary` paths in - `codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt:2084-2097` correct: 64tass resolves - `.binary` relative to the **including `.asm` file**, and the `.asm` is written to - `options.outputDir` (`AsmGen.kt:113`). - - **Invariant to state in code**: `OutputFiles.asm()` must resolve to the same directory - as `options.outputDir`. If those two ever diverge, `%asmbinary` silently breaks at - 64tass time. - - Note `outputDir` has non-artifact uses that this re-pointing also affects, and which - the later steps handle: `IRFileWriter.kt:297` (serialises it into the `.p8ir`, so the - standalone codegen tools write their `.asm` there), `CompilerMain.kt:151-152` - (directory creation), `CompilerDaemon.kt:147-158` (response file list). - -3. **New helper file `codeCore/src/prog8/code/core/OutputFiles.kt`** - single place that knows artifact naming: - - `outputStemOf(name: Path): String` - `"game.pgz"` -> `game`, `"game"` -> `game`, - `".pgz"` -> `.pgz` (a dot at index 0 is not an extension). **Applied only to - `options.outputFile`.** `program.name` is `filepath.nameWithoutExtension` - (`Compiler.kt:478`), so a source named `my.game.p8` has the stem `my.game` today; running - `outputStemOf` over it would rename every artifact and break the "identical to today" - guarantee. - - `fun CompilationOptions.programFile(defaultName: String): Path` - `outputFile` if set, else - legacy `outputDir.resolve(...)` per `options.output` (`Enumerations.kt:168-175`): - `PRG` -> `.prg`, `XEX` -> `.xex`, `RAW` -> `.bin`, `LIBRARY` -> `.bin`, `ELF` -> `.elf`, - `AMIGAHUNK` -> no extension (just the name). - **Must special-case `VMTarget`**: `VMTarget.kt:21` declares `defaultOutputType = OutputType.PRG` - but the artifact is `IRFileWriter.kt:46`'s `.p8ir`. - - `class OutputFiles(val programFile: Path, val stem: String, val dir: Path)` with - `asm()`, `list()`, `viceMonList()`, `binFile()`, `ir()`, `bankedCalls()`, `obj()`, - `linkScript()`. - - `viceMonList()` is `/.vice-mon-list` - the **stem**, never - `programFile + ".vice-mon-list"`. - - `binFile()` is needed for the stale-`.bin` cleanup at `AssemblyProgramM68k.kt:96`, - which is keyed on the aux stem, not on the artifact. - - Factory `OutputFiles.for(options, defaultName)`; legacy stem = `defaultName` verbatim, - `-o` stem = `outputStemOf(outputFile)`. - - **Must handle `programFile.parent == null`** (bare filename like `game.pgz`): fall back to - `outputDir`. (`Path("").resolve("x") == Path("x")`, so both spellings agree here.) - - **Move `C64Target.viceMonListName` (`C64Target.kt:26`) into `OutputFiles`** and delete it. - It is a C64-scoped companion helper currently called from 4 targets plus - `AssemblyProgram6502.kt:27`. - -4. **`.p8ir` round-trip**: emit `outputFile=` **only when non-null**, next to - `outputDir=` in `intermediate/src/prog8/intermediate/IRFileWriter.kt:297`, parse in - `IRFileReader.kt:172`, feed into the builder at `:191`. - - Must be `.absolute()`, exactly like `outputDir` at `:297`, or the standalone codegen tools - resolve it against their own cwd. - - Emitting unconditionally would break *every* `.p8ir` for older readers with - `illegal OPTION outputFile` (`IRFileReader.kt:176`), even for users who never pass `-o`. - Emitting only when set keeps the default path byte-identical. - - Blast radius of that error is wider than the two named tools: `prog8c -vm new.p8ir` - (`VMTarget.kt:83-86` -> `VirtualMachine.kt:3165`) and `-compareir` - (`CompilerMain.kt:527`) also read the file, so a new-compiler / old-binary combination - would fail. Acceptable; tools ship together. - - `IR_FORMAT_VERSION` (`IRFormat.kt:12`, currently `4`) needs **no** bump. It is validated - with strict string equality at `IRFileReader.kt:84-85` and is asserted by name in - `intermediate/test/TestIRFileInOut.kt:41` plus 7 further hand-written `IRFORMAT="4"` - fixtures - a bump means 10 coordinated edits for no benefit. - - Note `IRFileWriter` already has an unused `outfileOverride: Path?` parameter - (`IRFileWriter.kt:45`) that all three production call sites pass as `null` - (`VmCodeGen.kt:58`, `New6502CodeGenerator.kt:37`, `M68kCodeGenerator.kt:34`). Prefer - threading `OutputFiles.for(...)` through that parameter over adding resolution logic - inside `IRFileWriter`. - - `prog8-newgen` / `prog8-m68kgen` (`codeGenNew6502/src/.../Main.kt`, - `codeGenM68k/src/.../Main.kt`) only call `generate()`, never `assemble()`, so `-o` affects - only their `.asm` name. Note the *in-process* `New6502CodeGenerator.kt:44` / - `M68kCodeGenerator.kt:40` **do** return the assembly wrappers, so `-newcodegen` on cx16 - also goes through step 7. - -## Ordered steps - -5. **CLI** (`compiler/src/prog8/CompilerMain.kt`): - - New option `val outputFile by option("-o", "--output", help = "name of the output program file (and directory for the other output files)")` - declared between `-noopt` (`:90`) and `-out` (`:91`), matching the existing - `option("-x", "--x")` convention and `docs/source/compiling.rst:163-164`. - - Validation following the existing pattern (`presenter.printErrorLine(...)` + `return false`, - as at `:154-163`) - there is no separate validate step, checks are inline in `compileMain`: - - both `-o` and `-out` given -> error - - `-o` with >1 source file -> error - - `-o` pointing at an existing directory -> error - - Directory creation (`:151-152` is `val outputPath = pathFrom(outputDir); outputPath.createDirectories()`): - when `-o` is given, create `outputFilePath.parent` if non-null; otherwise keep `:151-152` - as-is. `Path(".")` creation is a no-op, so `-o` without `-out` is naturally safe. - - `pathFrom` (`:54`) does no tilde expansion. Decide deliberately whether `-o` should; - `-srcdirs` (`:165`) and `-target` (`:174`) do, `-out` does not. Either way, document it. - - **Not covered:** `-noasm` / `-check` / `-dumpsymbols` / `-gendoc` produce no artifact and - silently ignore `-o`. This is documented, not an error. - -6. **`CompilerArguments.outputFile: Path? = null`** (`compiler/src/prog8/compiler/Compiler.kt:70`, - right after `outputDir`). `CompilerArguments` is a plain `class`, not a `data class`. - The default keeps every existing call site and test compiling. **Four** production sites: - - `CompilerMain.kt:239` watch mode (`outputPath` arg at `:266`) - - `CompilerMain.kt:327` daemon client (`outputPath` arg at `:354`) - - `CompilerMain.kt:395` normal (`outputPath` arg at `:422`) - - `CompilerDaemon.kt:195` `toCompilerArguments` (all-named args, `outputDir = resolvedOutputDir` at `:222`) - - All sites pass everything from `cwd` onward by name and the preceding parameters positionally, - so inserting a parameter after `outputDir` is source-compatible - verify per site. - -7. **`codeCore/src/prog8/code/assembly/AssemblyProgram6502.kt`** (package `prog8.code.assembly`, - not `prog8.code.core.assembly`): - - The six `outputDir.resolve(...)` fields (`:23-28`) are **constructor initialisers**, and the - constructor takes `outputDir: Path` (`:16`) - `assemble()` (`:30`) receives `options` only - afterwards. The constructor must change: replace the `outputDir` parameter with - `CompilationOptions` (or add it), so the three construction sites still compile: - `codeGenCpu6502/.../AsmGen.kt:133`, `codeGenNew6502/.../New6502CodeGenerator.kt:44`, - `codeGenM68k/.../M68kCodeGenerator.kt:40`. - - Build `val outs = OutputFiles.for(options, name)` at the top of `assemble()` and drop the fields. - - All four branches (PRG `:43`, XEX `:65`, RAW `:86`, LIBRARY `:106`) pass `outs.programFile` - to `addRemainingOptions` (`:38`) instead of `prgFile`/`xexFile`/`binFile`; - `--labels=` (`:47/:69/:89/:110`) uses `outs.viceMonList()`, `--list=` (`:58/:80/:100/:121`) - uses `outs.list()`. - - `removeGeneratedLabelsFromMonlist()` (`:168`) and `generateBreakpointList()` (`:179`) take - **no parameters** and read the `viceMonListFile` *field* - i.e. they duplicate the path - independently of the `--labels=` string. They must receive the same `OutputFiles` instance - used to build `--labels=`, or `readLines()` at `:170` throws `NoSuchFileException`. - -8. **`codeGenM68k/.../AssemblyProgramM68k.kt`**: same treatment for `:18` (asm), `:20` - `elfFile()`, `:75` obj, `:76` / `:120` list, `:96` stale-`.bin` cleanup, `:99` link script, - `:119` extension-less hunk exe. `elfFile()` has exactly one caller (`:100`) and becomes a - private accessor over `OutputFiles`. Constructor change needed here too - (`M68kCodeGenerator.kt:40`). - -9. **`.asm` writers** (3 sites) and **`.p8ir` writer**: `codeGenCpu6502/.../AsmGen.kt:113`, - `codeGenNew6502/.../AsmGen.kt:120`, `codeGenM68k/.../AsmGen.kt:580`, - `intermediate/.../IRFileWriter.kt:46` - resolve via `OutputFiles` instead of - `"${program.name}.asm"` / `"${irProgram.name}.p8ir"`. The `.p8ir` name is written from three - places (`VmCodeGen.kt:58`, `New6502CodeGenerator.kt:37`, `M68kCodeGenerator.kt:34`) - all three - go through `IRFileWriter.kt:46`. - -10. **`.bankedcalls`**: `simpleAst/.../BankedCallUtils.kt:33` uses the aux stem. - Consequence to accept (not a bug): the "call-site IDs changed" read-back at `:36-57` stops - finding the old file when the output name changes, exactly as it already does with `-out`. - -11. **Emulator launchers - remove the stem hack.** Today `CompilerMain.kt:447-448` builds - `outputPath.resolve(name)` and `removeSuffix(".prg")` (a no-op there, since `compilerAst.name` - has no extension), and each launcher re-appends an extension or probes siblings. - Change `ICompilationTarget.launchEmulator(selectedEmulator, programFile, quiet)` - (`ICompilationTarget.kt:96`) to receive the **actual artifact path with extension**, and add - KDoc stating the contract (it is a public interface in the published `codeCore` module). - - `C64Target.kt:71-73`, `C128Target.kt:66-68`, `PETTarget.kt:65-67`: `-autostart `, - `-moncommands ` - - `Cx16Target.kt:68` (box16 `-sym `), `:79` (`-prg `) - - `Amiga500Target.kt:108`, `Amiga1200Target.kt:68`: pass the file as-is (drop the no-op - `resolveSibling("${fileName}")`) - - `Qemu68kTarget.kt:64-65`: use the artifact directly instead of probing siblings. The `.bin` - probe is already dead - `AssemblyProgramM68k.kt:96` deletes `$name.bin` and nothing produces - a `.bin` for qemu68k (m68k only ever yields `.elf`, `.o`, or the extension-less hunk exe). - Keep an error message if the artifact does not exist. - - `VMTarget.kt:74-76`: `launchEmulator` delegates to `launchEmulatorWithTrace` (`:78`), which - is a **public method not on the interface**, with three call sites: `CompilerMain.kt:374`, - `:456`, and `runVm` at `:513`. Move the `.p8ir` fallback **into `runVm`** and make - `launchEmulatorWithTrace` exact-path only. Reason: the fallback is currently shared by the - `-vm` flow (which needs it) and the `-emu` flow (which must not). Without this move, - `-o build/game -target virtual -emu` (extension-less `-o`) appends `.p8ir`, does not find - it, and throws an uncaught `NoSuchFileException` - a regression this plan would introduce. - - `viceMonListSibling()` must call the **same** `outputStemOf`, not - `Path.nameWithoutExtension` - the latter returns `""` for `.pgz` where `outputStemOf` - returns `.pgz`. - - Callers: `CompilerMain.kt:447-448` and `:368-369` (daemon) both pass the real path via - `OutputFiles.for(...)`. Steps 11 and 13 must land in the same commit: today the daemon - returns `[.prg, .asm, .p8ir]` and `CompilerMain.kt:368` takes `.first()`. - - Side effect worth recording: this also fixes `-emu`, which is **already broken today** for - `XEX`, `RAW` and `LIBRARY` - the launchers hardcode `.prg` regardless of `OutputType`. - - `ConfigFileTarget.launchEmulator` throws (`ConfigFileTarget.kt:185-187`) - unchanged, so - `-o` + custom target + `-emu` still dies with a raw exception. - -12. **`-compareir`**: `CompilerMain.kt:451` use `OutputFiles.for(...)`, taking the options from - `compilationResult.compilationOptions` - **not** from the CLI option, or `-o` + `-compareir` - compares the file against itself. Note `:451` currently uses the CLI-level `outputPath`, as - does `:447-448`. - -13. **Daemon**: add `outputFile: String?` to `DaemonRequest` (`DaemonProtocol.kt:32`), encode with - `propOpt("outputFile", req.outputFile)` after `outputDir` at `:80` (`propOpt` emits nothing when - null; the `append("null"); setLength(length-5)` placeholder hack at `:83-84` handles the - trailing comma), decode with `map["outputFile"] as? String` after `:147`. Backward/forward - compatible. Populate from `compilerArgs.outputFile` at `CompilerMain.kt:867`. - Plumb into `toCompilerArguments` (`CompilerDaemon.kt:186-227`), resolving relative to the - client cwd exactly like `outputDir` (`:189-194`). - Replace the hand-rolled name guessing at `CompilerDaemon.kt:147-158` with - `OutputFiles.for(result.compilationOptions, result.compilerAst.name)` - **program file first** - (current order is `[.prg, .asm, .p8ir]`; `CompilerMain.kt:368` uses `.first()`). - This also fixes its existing inaccuracies (it ignores `.xex`, `.bin`, `.elf`, `.list`, - `.vice-mon-list` and the extension-less Amiga hunk exe). - **Preserve the `writeAssembly` guard**: today's `.p8ir` entry is gated on - `!request.writeAssembly || target == VMTarget`. An unconditional list would make `-noasm` - report a nonexistent artifact that `.first()` then feeds to the emulator. - -14. **`prog8-m68kgen` result handling**: `codeGenM68k/src/.../Main.kt:26-27` ignores - `generate()`'s return value and always prints "Generated assembly: ...". With `-o` pointing - into a directory that may not exist, this silently lies. Also fix the hardcoded - `"${program.name}.asm"` in that message. - -## Tests - -New `compiler/test/TestOutputFileName.kt` (directly in `compiler/test/`, `package prog8tests.compiler` - -the `compiler/test/prog8tests/compiler/` folder contains a single unrelated file), plus additions to -`compiler/test/TestDaemonProtocol.kt`. - -- `OutputFiles` unit tests (pure, no compilation): - - `outputStemOf`: `game.pgz` -> `game`, `game` -> `game`, `.pgz` -> `.pgz`, `my.game` -> `my`. - - `programFile()` per `OutputType`: `PRG`/`.prg`, `XEX`/`.xex`, `RAW`/`.bin`, - `LIBRARY`/`.bin`, `ELF`/`.elf`, `AMIGAHUNK`/no extension, `VMTarget`/`.p8ir`. - - `OutputFiles.for(options, "prog")` with `outputFile = tmp/out/custom.pgz`: every accessor - resolves under `tmp/out` with stem `custom`; `viceMonList()` is `custom.vice-mon-list` - (never `custom.pgz.vice-mon-list`). - - **Legacy-stem regression guard**: with `outputFile = null`, a default name containing dots - (`my.game`) yields aux files `my.game.asm` etc., unchanged. - - bare `outputFile` (`game.pgz`, `parent == null`) resolves relative to `outputDir`. -- Compilation semantics via a **local** `compileTheThing`-style helper - `compileText` - (`compiler/test/helpers/compileXyz.kt:63`) and `compileFile` (`:12`) have no `outputFile` - parameter and no `CompilationOptions` escape hatch, so either add `outputFile: Path? = null` - to both (they have ~90 call sites) or copy the pattern from - `compiler/test/TestCompilerOnExamples.kt:55-83`. The test must create the parent directory - itself: `compileProgram` never calls `createDirectories()` (only `CompilerMain.kt:152` does). - Assert `out/custom.asm` exists, `out/on_the_fly_test_*.asm` does not, and that - `result.compilationOptions.outputFile` survives into `CompilationResult`. -- End-to-end artifact name: cx16 with `assemble = true` - 64tass must be on `PATH` - (installed in CI, `.github/workflows/all-ci.yml:15-19`; there is no skip mechanism, a missing - 64tass turns the test red, same as for every existing cx16 test). Precedent: - `compiler/test/codegeneration/TestVariableStepForLoops6502.kt:122` (NOT `TestArraysOfStructs.kt`, - whose `assemble = true` calls are all `VMTarget` and invoke no external assembler). - Assert `out/custom.pgz` exists with a 2-byte CBM load header and that no source-named `.prg` - was produced. m68k variant only with `writeAssembly=true, assemble=false` (vasm/vlink are **not** - in CI). -- `%asmbinary` under `-o`: compile a program with `%asmbinary` and `-o /x.prg`, with - `assemble = true`, and assert success. This is the only test that pins the - `.binary` / `.asm` co-location invariant. -- `virtual` target: `outputFile` = `out/ir.p8ir`, assert that file exists and the VM can run it. -- Daemon protocol encode/decode round-trip of the new field: update test 1 - (`TestDaemonProtocol.kt:11-75`, which enumerates every field) and assert `decoded.outputFile - shouldBe null` in test 2 (`:77-117`). -- Regression guard: `compiler/test/TestCompilerOnExamples.kt:109` `verifyOutputFileSize` builds - paths by hand - switch it to `OutputFiles.for(...)` so it keeps working, and expect **no** - size changes (all default paths are unchanged). Note these size checks are documented as - fragile (`TestCompilerOnExamples.kt:21-33`). -- **Not tested**: CLI option validation (`-o`+`-out`, `-o`+multiple files, `-o`+directory). - `compileMain` and `CompilerCli` are both `private` and no test in the repo invokes the CLI. - Accepted gap. - -## Risks / known consequences - -- Step 11 touches 9 launcher implementations and changes a public interface's parameter meaning; - it is the only genuinely invasive part. Required for `-o` + `-emu` to work, and it deletes the - fragile `removeSuffix(".prg")` idiom. Steps 11 + 13 land in the same commit; everything before - that is a separate commit. -- `%asmbinary` on a different drive/volume: `AsmGen.kt:2093` - `outputDir.sanitize().relativize(includedPath.sanitize())` throws `IllegalArgumentException` when - the two paths have different roots, which escapes to `Compiler.kt:372` as - `"\ninternal error"` + a rethrow. Pre-existing, but `-o D:\build\game.pgz` makes it reachable. - Add an explicit guard with a clear diagnostic. -- A program output name containing dots (`my.game`) yields aux stem `my`. Cosmetic, matches shell - conventions. Only applies to `-o`; the legacy path keeps its verbatim stem. -- `outputDir` and `outputFile` inside the `.p8ir` are absolute, so a `.p8ir` regenerated on - machine B sends `prog8-newgen` back to machine A's build directory. Pre-existing for - `outputDir`; `-o` doubles it. Consider falling back to the `.p8ir`'s own directory when the - recorded paths do not exist. -- Directory creation failure today produces a raw `IOException` stack trace, not a clean error - (`outputPath.createDirectories()` at `CompilerMain.kt:152` is unguarded). Worth a try/catch - while touching this code. -- `outputDir` used to be described as "only used for artifact paths"; that is not accurate - (it is also a `.binary` relativize base and is serialised into the `.p8ir`). The plan's design - survives that, but the invariant should be asserted by tests, not assumed. -- `AMIGAHUNK` has no extension, so the artifact name and the aux stem coincide; under `-o` that - means the artifact is `game` and the aux stem is `game`. -- `ConfigFileTarget.launchEmulator` throws - unaffected by the refactor. -- `assembler_options` in a target config can contain `--output` (`examples/customtarget/targetconfigs/f256.properties:63-65`); - today it is neutralised by ordering (`AssemblyProgram6502.kt:35-38` puts - `additionalAssemblerOptions` before `--output`). Keep that ordering and note it in `OutputFiles`. - -## Docs - -`docs/source/compiling.rst`: -- new `-o` / `--output` entry between `-noopt` (`:250`) and `-out` (`:254`) -- `:254-255` `-out`: note the mutual exclusion with `-o` -- fix the pre-existing bug at `:153` ("written to ``sourcefile.asm``" - it actually goes to the - output directory, which defaults to the CWD per `CompilerMain.kt:91`, and no `.asm` is produced - at all for `-target virtual`) -- `:163-164` "single dash or double dash" - satisfied by the `-o` / `--output` pair -- `:174` `-asmlist` -> `.list` naming -- `:436-448` VICE/box16 symbol-file sections: the real name is `.vice-mon-list`, not - `programname.vice-mon-list` -- note that `-o` is verbatim, that `%output` / `OutputType` still decides the content format, - and that `-o` is ignored with `-noasm` / `-check` / `-dumpsymbols` / `-gendoc` - -Also: `docs/source/technical.rst:111` and `programming.rst:1667` (`.bankedcalls`), -`binlibrary.rst:73` (`%output library` creates `.bin`), `profiling.rst:21,30` -(`-asmlist` listing naming), `docs/CODEBASE-KNOWLEDGE-GRAPH.md:116` (pipeline diagram), -a `docs/source/history.rst` changelog entry, and removing `docs/source/todo.rst:16`. - -## Verification - -```bash -gradle :compiler:compileKotlin --console=plain # quick -nice gradle build --console=plain # full, per AGENTS.md -prog8c -target cx16 -asmlist -o /tmp/p8out/game.pgz examples/cx16/hello_cx16.p8 && ls /tmp/p8out -prog8c -target cx16 -o /tmp/p8out/game examples/cx16/hello_cx16.p8 # extension-less -o -prog8c -target virtual -o /tmp/p8out/vm.p8ir -emu examples/virtual/hello.p8 -prog8c -target cx16 -o /tmp/x.prg -out /tmp/y examples/cx16/hello_cx16.p8 # must error -prog8c -target cx16 -o /tmp/p8out/game.pgz -emu examples/cx16/hello_cx16.p8 -diff <(prog8c -target cx16 -printast2 examples/cx16/hello_cx16.p8 >/dev/null; cat hello_cx16.prg | wc -c) /dev/null # default path unchanged -``` \ No newline at end of file diff --git a/intermediate/src/prog8/intermediate/IRFileReader.kt b/intermediate/src/prog8/intermediate/IRFileReader.kt index 838a87f03..358f55d21 100644 --- a/intermediate/src/prog8/intermediate/IRFileReader.kt +++ b/intermediate/src/prog8/intermediate/IRFileReader.kt @@ -150,6 +150,7 @@ class IRFileReader { var romable = false var noSysInit = false var outputDir = Path("") + var outputFile: Path? = null if(text.isNotBlank()) { text.lineSequence().forEach { line -> @@ -170,6 +171,7 @@ class IRFileReader { zpAllowed.add(UIntRange(zpstart.toUInt(), zpend.toUInt())) } "outputDir" -> outputDir = Path(value) + "outputFile" -> outputFile = Path(value) "optimize" -> optimize = value.toBoolean() "romable" -> romable = value.toBoolean() "noSysInit" -> noSysInit = value.toBoolean() @@ -189,6 +191,7 @@ class IRFileReader { .loadAddress(loadAddress) .memtopAddress(memtop) .outputDir(outputDir) + .outputFile(outputFile) .optimize(optimize) .noSysInit(noSysInit) .build() diff --git a/intermediate/src/prog8/intermediate/IRFileWriter.kt b/intermediate/src/prog8/intermediate/IRFileWriter.kt index a1a75ee30..a9b243ccb 100644 --- a/intermediate/src/prog8/intermediate/IRFileWriter.kt +++ b/intermediate/src/prog8/intermediate/IRFileWriter.kt @@ -7,7 +7,6 @@ import javax.xml.stream.XMLOutputFactory import javax.xml.stream.XMLStreamWriter import kotlin.io.path.absolute import kotlin.io.path.bufferedWriter -import kotlin.io.path.div private const val StMemorySlabBlockName = "prog8_slabs" @@ -43,7 +42,7 @@ private object IRStSymbolicReferenceXml { class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) { - private val outfile = outfileOverride ?: (irProgram.options.outputDir / ("${irProgram.name}.p8ir")) + private val outfile = outfileOverride ?: OutputFiles.of(irProgram.options, irProgram.name).ir() private val out = outfile.bufferedWriter(charset=Charsets.UTF_8) private val xml = XMLOutputFactory.newInstance().createXMLStreamWriter(out) private var numChunks = 0 @@ -295,6 +294,9 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) { emitLine("romable=${irProgram.options.romable}") emitLine("noSysInit=${irProgram.options.noSysInit}") emitLine("outputDir=${irProgram.options.outputDir.absolute()}") + // emit only when set (older readers throw on unknown OPTION keys) and it must be absolute: + // standalone codegen tools resolve the recorded path against their own cwd + irProgram.options.outputFile?.let { emitLine("outputFile=${it.absolute()}") } xml.writeEndElement() xml.writeCharacters("\n\n") } diff --git a/simpleAst/src/prog8/code/ast/BankedCallUtils.kt b/simpleAst/src/prog8/code/ast/BankedCallUtils.kt index 712fc4503..1dd8eb352 100644 --- a/simpleAst/src/prog8/code/ast/BankedCallUtils.kt +++ b/simpleAst/src/prog8/code/ast/BankedCallUtils.kt @@ -4,6 +4,7 @@ import prog8.code.StNodeType import prog8.code.SymbolTable import prog8.code.core.CompilationOptions import prog8.code.core.IErrorReporter +import prog8.code.core.OutputFiles import prog8.code.core.Position @@ -30,7 +31,7 @@ fun writeBankedCallsFile(program: PtProgram, symbolTable: SymbolTable, options: if (eligible.isEmpty()) return - val file = options.outputDir.resolve("${program.name}.bankedcalls") + val file = OutputFiles.of(options, program.name).bankedCalls() // check existing file for ID changes that might break overlay managers val existingFile = file.toFile()