Compare commits

..
47 Commits
Author SHA1 Message Date
Irmen de Jong 97dd081b02 Fix block alias 2026-10-22 19:00:44 +02:00
Irmen de Jong 2d3dc9d2d8 Fix 6502 codegen for pointer-to-pointer struct field double-deref 2026-10-19 19:29:10 +02:00
Irmen de Jong d183ba9f8f fix 6502 pointer deref clobbering bare pointer variable 2026-10-19 19:29:03 +02:00
Irmen de Jong ef39144d26 Fix virtual target typed pop results 2026-10-17 02:03:40 +02:00
Irmen de Jong 0aca77e234 harden AstWalker detached-subtree skipping and extract shared canSkipExplicitCmpi helper 2026-10-17 00:57:15 +02:00
Irmen de Jong 96a804ce3e fix wrong result for short-circuit boolean and/or on the virtual target 2026-10-17 00:35:52 +02:00
Irmen de Jong b71787a929 fix compiler crash on 'not' combined with or/and in one expression 2026-10-17 00:35:41 +02:00
Irmen de Jong ab8d924569 fix builtins rejecting a cx16 virtual register as their result register
cherry-picked from master e74c34a5
2026-10-01 03:41:29 +02:00
Irmen de Jong eafd8c62d2 fix unsound min/max rewrite when the target aliases an operand.
cherry-picked from master 35ebb499
2026-09-30 20:01:06 +02:00
Irmen de Jong ee09d35fdc fix byte 'downto 0' for-loop hanging on the virtual target
BSTPOS after DECM read stale status flags on the VM because DECM leaves
flags untouched there. Only use the BSTPOS fast path on 6502 hardware or
targets honoring the multi-byte status-bits contract; otherwise emit the
explicit LOADM+CMPI+BSTNE check. Backported from master (22a2f8cbc).
2026-09-12 01:36:51 +02:00
Irmen de Jong af14562e2b allow struct field array sizes to be constant expressions
fixes #240
2026-09-11 01:38:18 +02:00
Irmen de Jong ccc26a0eb2 Fix alias targets to enum members
Defer alias error reporting when the target is a not-yet-desugared
enum member. Also reject aliases that target an enum itself.

fixes #239
2026-09-10 19:20:02 +02:00
Irmen de Jong e017c541bb proper error message when using a builtin function name as block name. Also for some keywords if the parser allows. Fixes #236 2026-09-10 19:20:02 +02:00
Irmen de Jong 3d5a7795d0 make set of keywords (for name restriction test) dynamic 2026-09-10 19:20:02 +02:00
Irmen de Jong af06fdfffc fix more 6502-only struct pointer-field bugs; add indexed in-place support
fixes #237
2026-09-10 19:20:02 +02:00
Irmen de Jong e811ecdbde fix 6502 struct pointer-field array indexing 2026-09-09 23:47:09 +02:00
Irmen de Jong 30435b6778 version 12.3.3 2026-09-07 19:18:15 +02:00
Irmen de Jong 1358a535e3 Fix inliner crash when inlining a sub that assigns to its own parameter
Also error on a variable declaration directly followed by another statement on the same line.

fixes #235
2026-09-04 22:13:11 +02:00
Irmen de Jong c8f309307f fix inlining bug: subroutines with register-mapped parameters cannot be inlined 2026-09-02 19:10:21 +02:00
Irmen de Jong b702ea50da no longer mangle inline asm that uses pipe operator (in other spots that is used as an instruction separator...) 2026-09-02 02:02:53 +02:00
Irmen de Jong 05ba1237dc fix %asm line grouping that tripped the asm optimizer to think stuff was one line where it wasn't 2026-09-01 22:04:33 +02:00
Irmen de Jong 739fb6dc42 release 12.3.2 2026-08-30 22:39:01 +02:00
Irmen de Jong aa3f8e3038 fix daemon error 2026-08-28 05:14:12 +02:00
Irmen de Jong 2bbbacc46e normal error message instead of crash when typed pointer arrays are used as struct field (currently not supported by the compiler) 2026-08-24 19:21:30 +02:00
Irmen de Jong 296295011e Fix 6502 indexed pointer array store correctness bugs / missing code gen implementations 2026-08-23 20:54:32 +02:00
Irmen de Jong de20c09790 fix missing array bounds check for struct field arrays 2026-08-23 18:42:57 +02:00
Irmen de Jong 4912c96f78 various codegen bug fixes 2026-08-23 00:12:12 +02:00
Irmen de Jong 45cb50ebb1 fix crash on typecast of array element to long as operand of shift/add 2026-08-22 19:05:14 +02:00
Irmen de Jong 2cd2bb0121 optimized the 3d rotation examples 2026-08-22 19:03:14 +02:00
Irmen de Jong 65a1300147 cpu6502: implement pointer inplace invert/negate (byte/word/long/float) 2026-08-21 01:33:43 +02:00
Irmen de Jong ba6179a4d3 fix crash and errors on parenthesized/casted struct pointer field access (e.g. (t.next).flag, (t.port as ^^Thing).flag) 2026-08-21 00:48:19 +02:00
Irmen de Jong 6b91a464c0 fix spelling errors 2026-08-20 23:55:26 +02:00
Irmen de Jong 3aab457a47 fix const evaluation type truncation error for bitwise operations between constants of different types 2026-08-18 23:48:06 +02:00
Irmen de Jong 08b4168311 IR: preserve floating point value precision in struct fields 2026-08-16 21:30:25 +02:00
Irmen de Jong 36b12e9897 fix uword double increment/decrement bug 2026-08-16 07:26:21 +02:00
Irmen de Jong 9c2d8541c3 cx16: fix invalid code in sys.restore_irq(). Fixes #233 2026-08-16 03:33:40 +02:00
Irmen de Jong 48da22866c simplify and fix daemon output messages once more 2026-08-12 23:48:28 +02:00
Irmen de Jong 4dedc27e5b release 12.3.1 2026-08-10 20:54:26 +02:00
Irmen de Jong 5ba494c5e5 fix IR codegen needing explicit CMP instructions because of changed status bits contract (see commit #19238cc36) 2026-08-09 22:36:39 +02:00
Recoordinate 5c16ebe377 Fix typo in comment (#232) 2026-08-09 12:35:32 +02:00
Irmen de Jong b5dfc0cd97 fix debug message and tests 2026-08-09 00:32:58 +02:00
Irmen de Jong d276fb9ed5 fix the overdraw in filled disc drawing routines 2026-08-09 00:20:22 +02:00
Irmen de Jong ec9567a669 fix error output text in daemon mode 2026-08-08 16:44:43 +02:00
Irmen de Jong 70d81298c1 fix multi-value initializer error when in nested scope 2026-08-06 23:32:08 +02:00
Irmen de Jong 1de1c10b41 IR: fix register type error when 0-assigning into an array 2026-08-06 19:44:52 +02:00
Irmen de Jong 25f883bb85 fix dangling bullet in history list 2026-08-05 11:03:02 +02:00
Irmen de Jong adc6d6626a attempt to fix Windows library file import bug (InvalidPathException) 2026-08-05 10:34:52 +02:00
77 changed files with 3106 additions and 1191 deletions

No files matched your search

+1
View File
@@ -240,6 +240,7 @@ When writing inline assembly (`%asm {{ }}` blocks or `asmsub` routines), load th
- **No bare `{ }` blocks** like C/Java
- Indentation: 4 spaces for .p8 and .asm files (no tabs)
- Character encoding: 6502 targets use PETSCII by default (call `txt.lowercase()` at start for lowercase). Virtual target uses ISO (`%encoding iso` + `txt.iso()`)
- **Test programs for CBM targets must only use lowercase text in string literals** (the C64/C128/PET32/CX16 targets all use PETSCII). In PETSCII, uppercase letters A-Z are the *graphics* symbols, so any uppercase text you put in a string literal is rendered as unpredictable graphics characters instead of readable letters. Use only lowercase (a-z) for any printed test output, labels, or expected-match strings on these targets; reserve uppercase for intentional PETSCII graphics. The `virtual` target (ISO) is unaffected and may use either case.
- Array size inferred from initializer: `str[] types = ["a", "b", "c"]`
- Enums: `Enum::Value` syntax (double colon), declared inside a block. They are syntactic sugar for a list of `const` declarations, not a type. Use enums for related values, `const` for standalone
- Avoid `globals.XXXX` — move constants closer to where they're used
+2 -2
View File
@@ -15,7 +15,7 @@ which aims to provide many conveniences over raw assembly code (even when using
**Want to buy me a coffee or a pizza perhaps?**
This project was created over the last couple of years by dedicating a huge amoung of my free time to it, to make it the best I possibly can.
This project was created over the last couple of years by dedicating a huge amount of my free time to it, to make it the best I possibly can.
If you like Prog8, and think it's worth a nice cup of hot coffee or a delicious pizza,
you can help me out a little bit over at [ko-fi.com/irmen](https://ko-fi.com/irmen) or [PayPal](https://paypal.me/irmendejong)
@@ -94,7 +94,7 @@ What does Prog8 provide?
*Rapid edit-compile-run-debug cycle:*
- use a modern PC to do the work on, use nice editors and enjoy quick compilation times
- can automatically run the program in the Vice emulator after succesful compilation
- can automatically run the program in the Vice emulator after successful compilation
- breakpoints, that let the Vice emulator drop into the monitor if execution hits them
- source code labels automatically loaded in Vice emulator so it can show them in disassembly
@@ -219,6 +219,11 @@ class DataType private constructor(
fun elementToArray(splitwords: Boolean = true): DataType {
return if (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR) arrayFor(base, splitwords)
else if(base.isPointer) {
if(subType!=null) arrayOfPointersTo(subType)
else if(subTypeFromAntlr!=null) arrayOfPointersFromAntlrTo(sub, subTypeFromAntlr)
else arrayOfPointersTo(sub!!)
}
else arrayFor(base, false)
}
@@ -44,6 +44,11 @@ enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
* behavior consistent across all current targets.
*/
VIRTUAL(false)
;
/** True for the 6502-family targets (plain 6502 and 65C02). */
val is6502: Boolean
get() = this == CPU6502 || this == CPU65C02
}
interface ICompilationTarget: IStringEncoding, IMemSizer {
@@ -41,7 +41,7 @@ class AsmGen6502Internal (
internal val options: CompilationOptions,
internal val errors: IErrorReporter,
private var generatedLabelSequenceNumber: Int,
private val preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
preassignedCallSiteIds: Map<PtAsmSub, UByte> = emptyMap()
) {
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25,40,50,80,100)
@@ -238,12 +238,35 @@ class AsmGen6502Internal (
return e
}
// Try to get a static address for a simple variable or constant-indexed array element.
// Returns the asm label if the expression is a variable/array with constant index,
// or null if it is more complex. The requested variableMemSize must match the
// variable's actual size, otherwise null is returned to force the generic path
// that correctly handles size conversions (e.g. ubyte promoted to uword).
fun tryGetStaticAddress(expr: PtExpression, variableMemSize: Int): String? {
val e = unwrapCasts(expr)
if (e is PtIdentifier) return asmVariableName(e)
if (e is PtIdentifier) {
val actualSize = when {
e.type.isByte -> 1
e.type.isWord -> 2
e.type.isLong -> 4
e.type.isFloat -> options.compTarget.FLOAT_MEM_SIZE
else -> variableMemSize
}
if (actualSize != variableMemSize) return null
return asmVariableName(e)
}
if (e is PtArrayIndexer) {
val idx = e.index.asConstInteger()
if (idx != null && e.variable != null) {
val actualSize = when {
e.type.isByte -> 1
e.type.isWord -> 2
e.type.isLong -> 4
e.type.isFloat -> options.compTarget.FLOAT_MEM_SIZE
else -> variableMemSize
}
if (actualSize != variableMemSize) return null
val offset = program.memsizer.memorySize(e.type, idx)
if (offset + (variableMemSize - 1) < 256) return "${asmVariableName(e.variable!!)}+$offset"
}
@@ -770,7 +793,14 @@ class AsmGen6502Internal (
lda #$${hex.take(2)}
sta ${target.asmVarname}+3""")
}
TargetStorageKind.ARRAY -> TODO("assign long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
pointerGen.assignLong(IndexedPtrTarget(target), value.asConstInteger()!!)
} else {
TODO("assign long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> pointerGen.assignLong(target.pointer!!, value.number.toInt())
@@ -793,7 +823,14 @@ class AsmGen6502Internal (
)
}
TargetStorageKind.ARRAY -> TODO("assign long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
pointerGen.assignLongVar(IndexedPtrTarget(target), asmSymbolName(value))
} else {
TODO("assign long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> pointerGen.assignLongVar(target.pointer!!, asmSymbolName(value))
@@ -808,10 +845,23 @@ class AsmGen6502Internal (
out(" lda $valuesym | sta ${target.asmVarname}")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> TODO("assign typecasted long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> TODO("assign typecasted long into pointer ${target.position}")
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
} else if(value.value.type.isWord) {
@@ -824,7 +874,16 @@ class AsmGen6502Internal (
sta ${target.asmVarname}+1""")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> TODO("assign typecasted long to array ${target.position}")
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
@@ -836,7 +895,11 @@ class AsmGen6502Internal (
sta cx16.$startreg+1""")
signExtendLongVariable("cx16.$startreg", value.value.type.base)
}
TargetStorageKind.POINTER -> TODO("assign typecasted long into pointer ${target.position}")
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
} else if(value.value.type.isFloat) {
@@ -877,7 +940,109 @@ class AsmGen6502Internal (
}
} else throw AssemblyError("weird casted type ${value.value.type} as ${value.type} at $value")
} else {
TODO("assign typecasted expression $value to a long target ${target.kind} at ${target.position} - please report this issue. Use simple expressions and temporary variables for now")
// inner value is not a plain identifier (e.g. an array element or function result)
when {
value.value.type.isByte -> {
when (target.kind) {
TargetStorageKind.VARIABLE -> {
assignExpressionToRegister(value.value, RegisterOrPair.A, false)
out(" sta ${target.asmVarname}")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> assignExpressionToRegister(value, target.register!!, true)
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
value.value.type.isWord -> {
when (target.kind) {
TargetStorageKind.VARIABLE -> {
assignExpressionToRegister(value.value, RegisterOrPair.AY, value.value.type.isSigned)
out("""
sta ${target.asmVarname}
sty ${target.asmVarname}+1""")
signExtendLongVariable(target.asmVarname, value.value.type.base)
}
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
// generic fallback: evaluate full long value into register then store via indexed pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
} else {
TODO("assign typecasted long to array ${target.position}")
}
}
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
val startreg = target.register!!.startregname()
assignExpressionToRegister(value.value, RegisterOrPair.AY, value.value.type.isSigned)
out("""
sta cx16.$startreg
sty cx16.$startreg+1""")
signExtendLongVariable("cx16.$startreg", value.value.type.base)
}
TargetStorageKind.POINTER -> {
// generic fallback for pointer
assignmentAsmGen.assignExpressionToRegister(value, RegisterOrPair.R12R13, true)
assignmentAsmGen.assignRegisterLong(target, RegisterOrPair.R12R13)
}
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
value.value.type.isFloat -> {
assignExpressionToRegister(value.value, RegisterOrPair.FAC1)
when (target.kind) {
TargetStorageKind.VARIABLE -> {
out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta ${target.asmVarname}
lda floats.FAC_ADDR+3
sta ${target.asmVarname}+1
lda floats.FAC_ADDR+2
sta ${target.asmVarname}+2
lda floats.FAC_ADDR+1
sta ${target.asmVarname}+3""")
}
TargetStorageKind.REGISTER -> {
require(target.register in CombinedLongRegisters)
val startreg = target.register!!.startregname()
out("""
jsr floats.QINT
lda floats.FAC_ADDR+4
sta cx16.$startreg
lda floats.FAC_ADDR+3
sta cx16.$startreg+1
lda floats.FAC_ADDR+2
sta cx16.$startreg+2
lda floats.FAC_ADDR+1
sta cx16.$startreg+3""")
}
TargetStorageKind.ARRAY -> TODO("assign typecasted float to long array ${target.position}")
TargetStorageKind.MEMORY -> throw AssemblyError("memory is bytes not long ${target.position}")
TargetStorageKind.POINTER -> TODO("assign typecasted float to long pointer ${target.position}")
TargetStorageKind.VOID -> { /* do nothing */ }
}
}
else -> throw AssemblyError("weird casted type ${value.value.type} as ${value.type} at $value")
}
}
} else if(target.kind == TargetStorageKind.REGISTER) {
assignExpressionToRegister(value, target.register!!, true)
@@ -1333,7 +1498,7 @@ $repeatLabel""")
}
else if(swap.target1.pointerDeref!=null && swap.target2.pointerDeref!=null) {
val v1 = swap.target1.pointerDeref!!
val v2 = swap.target1.pointerDeref!!
val v2 = swap.target2.pointerDeref!!
if(v1.derefLast && v1.chain.isEmpty() && v2.derefLast && v2.chain.isEmpty() && isZpVar(v1.startpointer) && isZpVar(v2.startpointer)) {
// optimized case where v1 and v2 are both already zeropage pointer variables
val name1 = asmVariableName(v1.startpointer)
@@ -1863,7 +2028,11 @@ $repeatLabel""")
if(asm.isIR)
throw AssemblyError("%asm containing IR code cannot be translated to 6502 assembly")
else
assembly.add(asm.assembly.trimEnd().trimStart('\r', '\n'))
// User-supplied assembly should be passed through verbatim. It may contain
// characters such as '|' (64tass bitwise OR) that out() would otherwise
// misinterpret as a statement separator, and the user's indentation/spacing
// should be preserved rather than re-tabbed.
assembly.add(asm.assembly.trim('\r', '\n'))
}
private fun translate(incbin: PtIncludeBinary) {
@@ -1988,6 +2157,8 @@ $repeatLabel""")
internal fun isZpVar(variable: PtIdentifier): Boolean = allocator.isZpVar(variable.name)
internal fun isZpVar(scopedName: String): Boolean = allocator.isZpVar(scopedName)
internal fun jmp(asmLabel: String, indirect: Boolean=false, indexedX: Boolean=false) {
if(indirect) {
if(indexedX)
@@ -169,6 +169,18 @@ internal fun String.hasInstr(mnemonic: String): Boolean {
return false
}
private fun replaceMnemonic(raw: String, oldMnemonic: String, newMnemonic: String): String {
val trimmed = raw.trimStart()
val instruction = trimmed.instructionPart()
if(!instruction.startsWith(oldMnemonic))
return raw.replaceFirst(oldMnemonic, newMnemonic)
val instrStartInTrimmed = trimmed.indexOf(instruction)
if(instrStartInTrimmed<0) return raw.replaceFirst(oldMnemonic, newMnemonic)
val leadingLen = raw.length - trimmed.length
val idxInRaw = leadingLen + instrStartInTrimmed
return raw.substring(0, idxInRaw) + newMnemonic + raw.substring(idxInRaw + oldMnemonic.length)
}
/** Extracts the instruction part of a trimmed assembly line, skipping any label prefix.
* For "mylabel: lda #1" returns "lda #1". For "lda #1" returns "lda #1". */
private fun String.instructionPart(): String {
@@ -673,7 +685,8 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Trimme
}
else if (tfirst.hasInstr("jsr") && tsecond.hasInstr("rts")) {
if(!tfirst.contains("floats.pushFAC") && !tfirst.contains("floats.popFAC")) { // these 2 routines depend on being called with JSR!!
mods += Modification(lines[0].index, false, lines[0].value.replace("jsr", "jmp"))
val replacement = replaceMnemonic(lines[0].value, "jsr", "jmp")
mods += Modification(lines[0].index, false, replacement)
mods += Modification(lines[1].index, true, null)
}
}
@@ -750,7 +763,7 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Trimme
val firstMnemonic = tfirst.take(3)
branchInversions[firstMnemonic]?.let { invertedBranch ->
if (sameLabel(tfirst, tsecond, tthird)) {
val branch = lines[1].value.replace("jmp", invertedBranch)
val branch = replaceMnemonic(lines[1].value, "jmp", invertedBranch)
mods.add(Modification(lines[0].index, true, null))
mods.add(Modification(lines[1].index, false, branch))
}
@@ -20,6 +20,25 @@ import prog8.codegen.cpu6502.assignment.*
translateFunctioncall(fcall, null, discardResult = true)
}
/**
* Several builtins can only produce their result in a CPU register, and which one that is
* depends on the width of the result: the byte-returning ones (lsb/msb) take a single register,
* the word-returning ones (lsw/msw) and popw take a register pair, and the builtin picks the
* specific register. [hardwareReg] is the register to fall back on when the caller has no
* preference, or when it asked for something the builtin cannot deliver to directly.
*
* The caller's requested register is honoured when it is a real CPU register. A CX16 virtual
* register (for example an asmsub parameter declared as @R0) is not: those are memory locations,
* so the builtin produces its result in [hardwareReg] and the caller's assignment then stores it
* into the virtual register. Passing the virtual register through would make the builtin reject
* it as an invalid result register.
*
* (funcPeekW and funcMkword already handle virtual registers themselves.)
*/
private fun withResultRegister(requested: RegisterOrPair?, hardwareReg: RegisterOrPair, compute: (RegisterOrPair) -> Array<RegisterOrPair>): Array<RegisterOrPair> =
if (requested != null && requested !in Cx16VirtualRegisters) compute(requested)
else compute(hardwareReg)
private fun translateFunctioncall(fcall: PtFunctionCall, firstReturnRegister: RegisterOrPair?, discardResult: Boolean): Array<RegisterOrPair> {
// Note: returns the actual register(s) that the return value is in, so the caller should still make sure to place it into the actual correct destination.
@@ -31,12 +50,12 @@ import prog8.codegen.cpu6502.assignment.*
val sscope = fcall.definingISub()
return when (fcall.name) {
"msw" -> funcMsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
"lsw" -> funcLsw(fcall, firstReturnRegister ?: RegisterOrPair.AY)
"msb" -> funcMsb(fcall, firstReturnRegister ?: RegisterOrPair.A)
"msb__long" -> funcMsbLong(fcall, firstReturnRegister ?: RegisterOrPair.A)
"lsb" -> funcLsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
"lsb__long" -> funcLsb(fcall,true, firstReturnRegister ?: RegisterOrPair.A)
"msw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcMsw(fcall, it) }
"lsw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcLsw(fcall, it) }
"msb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, false, it) }
"msb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcMsb(fcall, true, it) }
"lsb" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, false, it) }
"lsb__long" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcLsb(fcall, true, it) }
"mkword" -> funcMkword(fcall, firstReturnRegister ?: RegisterOrPair.AY)
"mklong", "mklong2" -> funcMklong(fcall, firstReturnRegister ?: RegisterOrPair.R14R15)
"clamp__byte", "clamp__ubyte" -> funcClamp(fcall)
@@ -98,8 +117,8 @@ import prog8.codegen.cpu6502.assignment.*
"pushw" -> funcPushW(fcall)
"pushl" -> funcPushL(fcall)
"pushf" -> funcPushF(fcall)
"pop" -> funcPop(firstReturnRegister ?: RegisterOrPair.A)
"popw" -> funcPopW(firstReturnRegister ?: RegisterOrPair.AY)
"pop" -> withResultRegister(firstReturnRegister, RegisterOrPair.A) { funcPop(it) }
"popw" -> withResultRegister(firstReturnRegister, RegisterOrPair.AY) { funcPopW(it) }
"popl" -> funcPopL()
"popf" -> funcPopF()
else -> throw AssemblyError("sizeof must have been replaced with a constant ${fcall.position}")
@@ -1194,7 +1213,7 @@ import prog8.codegen.cpu6502.assignment.*
val arg = fcall.args.single()
if(arg is PtIdentifier) {
val varname = asmgen.asmVariableName(arg)
when(val dt = arg.type.base) {
when(arg.type.base) {
BaseDataType.UBYTE -> {
asmgen.out(" lda $varname | jsr prog8_lib.func_sign_ub_into_A")
return arrayOf(RegisterOrPair.A)
@@ -1975,25 +1994,60 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcClamp(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
when {
fcall.type.isByte -> {
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W1+1", fcall.args[2].type) // maximum
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) // value
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
// simple values (number/variable) do not clobber W1, so min/max can be evaluated first
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W1+1", fcall.args[2].type) // maximum
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) // value
} else {
// any non-simple argument may use W1 as a temporary, so evaluate value and minimum
// first and preserve them while the maximum is evaluated
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) // value first (may use W1 as temp)
asmgen.out(" pha")
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha")
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W1+1", fcall.args[2].type) // maximum (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1")
asmgen.out(" pla")
}
asmgen.out(" jsr prog8_lib.func_clamp_${fcall.type.toString().lowercase()}") // result in A
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
} else {
// any non-simple argument may use P8ZP_SCRATCH_W1/W2 as temporaries (e.g. via multiply_words/divmod_w_asm),
// so evaluate value and minimum first and preserve them while the maximum is evaluated
assignAsmGen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) // value
asmgen.out(" pha\n tya\n pha") // save AY on stack (A low, Y high)
assignAsmGen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W1", fcall.args[1].type) // minimum
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha\n lda P8ZP_SCRATCH_W1+1\n pha") // save minimum
assignAsmGen.assignExpressionToVariable(fcall.args[2], "P8ZP_SCRATCH_W2", fcall.args[2].type) // maximum (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1+1\n pla\n sta P8ZP_SCRATCH_W1") // restore minimum
asmgen.out(" pla\n tay\n pla") // restore AY
}
asmgen.out(" jsr prog8_lib.func_clamp_${fcall.type.toString().lowercase()}") // result in AY
return arrayOf(RegisterOrPair.AY)
}
fcall.type.isLong -> {
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R10R11, true) // low
asmgen.assignExpressionToRegister(fcall.args[2], RegisterOrPair.R12R13, true) // high
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true) // val
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck) && fcall.args[2].isSimple(zpCheck)) {
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R10R11, true) // low
asmgen.assignExpressionToRegister(fcall.args[2], RegisterOrPair.R12R13, true) // high
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true) // val
} else {
// same ordering issue: value may clobber R10R11/R12R13 via multiply_longs/div_longs
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R14R15, true) // val first
asmgen.out(" lda cx16.r14\n pha\n lda cx16.r14+1\n pha\n lda cx16.r15\n pha\n lda cx16.r15+1\n pha")
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R10R11, true) // low
asmgen.assignExpressionToRegister(fcall.args[2], RegisterOrPair.R12R13, true) // high
asmgen.out(" pla\n sta cx16.r15+1\n pla\n sta cx16.r15\n pla\n sta cx16.r14+1\n pla\n sta cx16.r14")
}
asmgen.out(" jsr prog8_lib.func_clamp_long")
return arrayOf(RegisterOrPair.R14R15)
}
@@ -2003,11 +2057,20 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcMin(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
when {
fcall.type.isByte -> {
if (signed) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
} else {
// left or right may use B1/REG as temp (e.g. multiply), so save left
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
}
asmgen.out("""
lda P8ZP_SCRATCH_B1
sec
@@ -2020,8 +2083,18 @@ import prog8.codegen.cpu6502.assignment.*
+ lda P8ZP_SCRATCH_B1
+""")
} else {
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_B1", fcall.type) // right
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // left
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_B1", fcall.type) // right
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // left
} else {
// right in B1, left in A - left may use B1 as temp, so save right
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_B1", fcall.type) // right
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A) // left (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
// now A holds left, need to compare again? Actually left already in A, right in B1 restored, so cmp is still valid
// Re-do cmp after restore is not needed, the following cmp uses P8ZP_SCRATCH_B1 correctly
}
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" bcc +")
asmgen.out("""
@@ -2031,8 +2104,15 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha\n lda P8ZP_SCRATCH_W1+1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1+1\n pla\n sta P8ZP_SCRATCH_W1")
}
if(signed) {
asmgen.out("""
lda P8ZP_SCRATCH_W1
@@ -2106,11 +2186,19 @@ import prog8.codegen.cpu6502.assignment.*
private fun funcMax(fcall: PtFunctionCall): Array<RegisterOrPair> {
val signed = fcall.type.isSigned
val zpCheck = { name: String -> asmgen.isZpVar(name) }
when {
fcall.type.isByte -> {
if (signed) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_REG", fcall.type) // right (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
}
asmgen.out("""
lda P8ZP_SCRATCH_B1
sec
@@ -2123,8 +2211,15 @@ import prog8.codegen.cpu6502.assignment.*
+ lda P8ZP_SCRATCH_B1
+""")
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.A) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.A) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_B1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_B1\n pha")
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.A) // right (may clobber B1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_B1")
}
asmgen.out(" cmp P8ZP_SCRATCH_B1")
asmgen.out(" bcs +")
asmgen.out("""
@@ -2134,8 +2229,15 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.A)
}
fcall.type.isWord -> {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
if (fcall.args[0].isSimple(zpCheck) && fcall.args[1].isSimple(zpCheck)) {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right
} else {
asmgen.assignExpressionToVariable(fcall.args[0], "P8ZP_SCRATCH_W1", fcall.type) // left
asmgen.out(" lda P8ZP_SCRATCH_W1\n pha\n lda P8ZP_SCRATCH_W1+1\n pha")
asmgen.assignExpressionToVariable(fcall.args[1], "P8ZP_SCRATCH_W2", fcall.type) // right (may clobber W1)
asmgen.out(" pla\n sta P8ZP_SCRATCH_W1+1\n pla\n sta P8ZP_SCRATCH_W1")
}
if (signed) {
asmgen.out("""
lda P8ZP_SCRATCH_W1
@@ -2343,55 +2445,36 @@ import prog8.codegen.cpu6502.assignment.*
return arrayOf(RegisterOrPair.AY)
}
private fun funcMsbLong(fcall: PtFunctionCall, resultReg: RegisterOrPair): Array<RegisterOrPair> {
private fun funcMsb(fcall: PtFunctionCall, fromLong: Boolean, resultReg: RegisterOrPair): Array<RegisterOrPair> {
val arg = fcall.args.single()
if (!arg.type.isLong)
throw AssemblyError("msb__long requires long argument")
if (arg is PtNumber)
throw AssemblyError("msb(const) should have been const-folded away")
if (arg is PtIdentifier) {
val sourceName = asmgen.asmVariableName(arg)
when(resultReg) {
RegisterOrPair.A -> asmgen.out(" lda $sourceName+3")
RegisterOrPair.X -> asmgen.out(" ldx $sourceName+3")
RegisterOrPair.Y -> asmgen.out(" ldy $sourceName+3")
else -> throw AssemblyError("invalid register for msb long: $resultReg ${arg.position}")
}
} else if(arg is PtArrayIndexer) {
TODO("msb of long array element ${fcall.position}")
if(fromLong) {
if (!arg.type.isLong)
throw AssemblyError("msb__long requires long argument")
} else {
asmgen.assignExpressionToRegister(arg, RegisterOrPair.R14R15, arg.type.isSigned)
when(resultReg) {
RegisterOrPair.A -> asmgen.out(" lda cx16.r14+3")
RegisterOrPair.X -> asmgen.out(" ldx cx16.r14+3")
RegisterOrPair.Y -> asmgen.out(" ldy cx16.r14+3")
else -> throw AssemblyError("invalid register for msb long: $resultReg ${arg.position}")
}
if (!arg.type.isWord && !arg.type.isPointer)
throw AssemblyError("msb requires word argument")
}
return arrayOf(resultReg)
}
private fun funcMsb(fcall: PtFunctionCall, resultReg: RegisterOrPair): Array<RegisterOrPair> {
val arg = fcall.args.single()
if (!arg.type.isWord)
throw AssemblyError("msb requires word argument")
if (arg is PtNumber)
throw AssemblyError("msb(const) should have been const-folded away")
val offset = if(fromLong) 3 else 1
if (arg is PtIdentifier) {
val sourceName = asmgen.asmVariableName(arg)
when(resultReg) {
RegisterOrPair.A -> asmgen.out(" lda $sourceName+1")
RegisterOrPair.X -> asmgen.out(" ldx $sourceName+1")
RegisterOrPair.Y -> asmgen.out(" ldy $sourceName+1")
RegisterOrPair.AX -> asmgen.out(" lda $sourceName+1 | ldx #0")
RegisterOrPair.AY -> asmgen.out(" lda $sourceName+1 | ldy #0")
RegisterOrPair.XY -> asmgen.out(" ldx $sourceName+1 | ldy #0")
else -> throw AssemblyError("invalid register for msb: $resultReg ${arg.position}")
RegisterOrPair.A -> asmgen.out(" lda $sourceName+$offset")
RegisterOrPair.X -> asmgen.out(" ldx $sourceName+$offset")
RegisterOrPair.Y -> asmgen.out(" ldy $sourceName+$offset")
RegisterOrPair.AX -> asmgen.out(" lda $sourceName+$offset | ldx #0")
RegisterOrPair.AY -> asmgen.out(" lda $sourceName+$offset | ldy #0")
RegisterOrPair.XY -> asmgen.out(" ldx $sourceName+$offset | ldy #0")
else -> throw AssemblyError("invalid register for msb${if(fromLong) " long" else ""}: $resultReg ${arg.position}")
}
} else {
if(arg is PtArrayIndexer) {
if(fromLong)
TODO("msb of long array element ${fcall.position}")
// just read the msb byte out of the word array
if(arg.splitWords) {
if(arg.variable==null)
@@ -2413,15 +2496,15 @@ import prog8.codegen.cpu6502.assignment.*
RegisterOrPair.AX -> {
asmgen.loadScaledArrayIndexIntoRegister(arg, CpuRegister.Y)
asmgen.out(" lda $arrayVar,y | ldx #0")
}
}
RegisterOrPair.AY -> {
asmgen.loadScaledArrayIndexIntoRegister(arg, CpuRegister.Y)
asmgen.out(" lda $arrayVar,y | ldy #0")
}
}
RegisterOrPair.XY -> {
asmgen.loadScaledArrayIndexIntoRegister(arg, CpuRegister.X)
asmgen.out(" ldy $arrayVar,x | ldx #0")
}
}
else -> throw AssemblyError("invalid register for msb: $resultReg ${arg.position}")
}
} else {
@@ -2457,6 +2540,14 @@ import prog8.codegen.cpu6502.assignment.*
else -> throw AssemblyError("invalid register for msb: $resultReg ${arg.position}")
}
}
} else if(fromLong) {
asmgen.assignExpressionToRegister(arg, RegisterOrPair.R14R15, arg.type.isSigned)
when(resultReg) {
RegisterOrPair.A -> asmgen.out(" lda cx16.r14+$offset")
RegisterOrPair.X -> asmgen.out(" ldx cx16.r14+$offset")
RegisterOrPair.Y -> asmgen.out(" ldy cx16.r14+$offset")
else -> throw AssemblyError("invalid register for msb long: $resultReg ${arg.position}")
}
} else {
asmgen.assignExpressionToRegister(arg, RegisterOrPair.AY)
asmgen.out(" tya")
@@ -2471,7 +2562,7 @@ import prog8.codegen.cpu6502.assignment.*
if(fromLong) {
if (!arg.type.isLong) throw AssemblyError("lsb__long requires long")
} else {
if (!arg.type.isWord) throw AssemblyError("lsb requires word")
if (!arg.type.isWord && !arg.type.isPointer) throw AssemblyError("lsb requires word")
}
if (arg is PtNumber)
throw AssemblyError("lsb(const) should have been const-folded away")
@@ -377,7 +377,7 @@ internal class AssignmentAsmGen(
throw AssemblyError("weird array element type for pointer indexed read $elementDt")
}
} else {
val (zpPtrVar, _) = pointergen.deref(pointerDeref, addOffsetToPointer = true)
val (zpPtrVar, _) = pointergen.deref(pointerDeref, addOffsetToPointer = true, loadPointerFieldValue = true)
when {
elementDt.isByteOrBool -> {
@@ -2231,6 +2231,12 @@ internal class AssignmentAsmGen(
}
private fun optimizedPlusMinLongExpr(expr: PtBinaryExpression, target: AsmAssignTarget): Boolean {
// Generic fallback for indexed pointer arrays: route to generic codegen (less efficient but avoids missing specialized cases)
if(target.kind==TargetStorageKind.ARRAY && target.array?.pointerderef!=null) {
asmgen.assignExpressionToRegister(expr, RegisterOrPair.R12R13, true)
assignRegisterLong(target, RegisterOrPair.R12R13)
return true
}
val left = expr.left
val right = expr.right
when(right) {
@@ -2777,8 +2783,8 @@ internal class AssignmentAsmGen(
"&" -> {
when {
value == 0 -> asmgen.out(" lda #0 | tay")
value == 0x00ff -> asmgen.out(" lda #0")
value == 0xff00 -> asmgen.out(" ldy #0")
value == 0x00ff -> asmgen.out(" ldy #0")
value == 0xff00 -> asmgen.out(" lda #0")
value and 255 == 0 -> asmgen.out(" tya | and #>$value | tay | lda #0")
value < 0x0100 -> asmgen.out(" and #<$value | ldy #0")
else -> asmgen.out(" and #<$value | tax | tya | and #>$value | tay | txa")
@@ -4711,7 +4717,13 @@ $endLabel""")
else throw AssemblyError("only combined vreg allowed as long target ${target.position}")
}
TargetStorageKind.ARRAY -> {
asmgen.loadScaledArrayIndexIntoRegister(target.array!!, CpuRegister.Y)
val deref = target.array!!.pointerderef
if(deref!=null) {
// indexed pointer array via struct (e.g. foo.longs[i])
pointergen.assignLongReg(IndexedPtrTarget(target), pairedRegisters)
return
}
asmgen.loadScaledArrayIndexIntoRegister(target.array, CpuRegister.Y)
val arrayVarName = asmgen.asmSymbolName(target.array.variable!!)
val startreg = pairedRegisters.startregname()
// Unrolled copy to preserve array index in Y
@@ -5940,6 +5952,7 @@ $endLabel""")
eor #255
sta ${target.asmVarname}+3""")
}
TargetStorageKind.POINTER -> pointergen.inplaceLongInvert(PtrTarget(target))
else -> TODO("LONG INVERT ${target.kind} ${target.position}")
}
}
@@ -62,6 +62,63 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
}
}
private fun inplaceModificationIndexedPointerField(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
val array = target.array!!
val elementDt = target.datatype
if(!elementDt.isByteOrBool && !elementDt.isWord)
throw AssemblyError("inplace modification indexed pointer field for type $elementDt not supported")
val indexedTarget = IndexedPtrTarget(target)
val ptr = ptrgen.indexedEffectiveAddress(indexedTarget)
val elementTemp = asmgen.createTempVarReused(elementDt.base, false, array)
if(elementDt.isByteOrBool) {
asmgen.out("""
ldy #0
lda ($ptr),y
sta $elementTemp""")
} else {
asmgen.out("""
ldy #0
lda ($ptr),y
sta $elementTemp
iny
lda ($ptr),y
sta $elementTemp+1""")
}
// save the computed address on the hardware stack so later code can't clobber it
asmgen.out("""
lda $ptr
pha
lda $ptr+1
pha""")
// reuse the existing variable-target logic to modify the temp
val tempTarget = AsmAssignTarget(TargetStorageKind.VARIABLE, asmgen, elementDt, null, target.position, variableAsmName = elementTemp)
inplaceModification(tempTarget, operator, value)
// restore the address and store the temp back
asmgen.out("""
pla
sta $ptr+1
pla
sta $ptr""")
if(elementDt.isByteOrBool) {
asmgen.out("""
ldy #0
lda $elementTemp
sta ($ptr),y""")
} else {
asmgen.out("""
ldy #0
lda $elementTemp
sta ($ptr),y
iny
lda $elementTemp+1
sta ($ptr),y""")
}
}
private fun inplaceModification(target: AsmAssignTarget, operator: String, value: AsmAssignSource) {
// the asm-gen code can deal with situations where you want to assign a byte into a word.
@@ -247,7 +304,7 @@ internal class AugmentableAssignmentAsmGen(private val program: PtProgram,
TargetStorageKind.ARRAY -> {
val deref = target.array!!.pointerderef
if(deref!=null) {
TODO("inplace modification array indexed pointer deref ${target.position}")
inplaceModificationIndexedPointerField(target, operator, value)
return
}
val targetArrayVar = target.array.variable!!
@@ -113,34 +113,134 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
internal fun inplaceByteInvert(target: PtrTarget) {
TODO("inplace byte invert pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
val eorVal = if(target.dt.isBool) 1 else 255
if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02)) {
asmgen.out(" lda ($zpPtrVar) | eor #$eorVal | sta ($zpPtrVar)")
} else {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #$eorVal
sta ($zpPtrVar),y""")
}
}
internal fun inplaceWordInvert(target: PtrTarget) {
TODO("inplace word invert pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y""")
}
internal fun inplaceLongInvert(target: PtrTarget) {
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
eor #255
sta ($zpPtrVar),y""")
}
internal fun inplaceByteNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace byte negate to pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
if(asmgen.isTargetCpu(CpuType.CPU65C02)) {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
ina
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
eor #255
clc
adc #1
sta ($zpPtrVar),y""")
}
}
internal fun inplaceWordNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace word negate pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
sec
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
internal fun inplaceLongNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace long negate pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
asmgen.out("""
ldy #$offset
sec
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y
iny
lda #0
sbc ($zpPtrVar),y
sta ($zpPtrVar),y""")
}
internal fun inplaceFloatNegate(target: PtrTarget, scope: IPtSubroutine?) {
TODO("inplace float negate pointer deref ${target.position}")
val (zpPtrVar, offset) = deref(target.pointer)
// flip sign bit in second byte of 5-byte float
asmgen.out("""
ldy #$offset
iny
lda ($zpPtrVar),y
eor #${'$'}80
sta ($zpPtrVar),y""")
}
internal fun deref(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false): Pair<String, UByte> {
// walk the pointer deref chain and leaves the final pointer value in a ZP var
// this will often be the temp var P8ZP_SCRATCH_PTR but can also be the original pointer variable if it is already in zeropage and there is nothing to add to it
// returns the ZP var to use as a pointer, and a Y register offset (which can be zero)
/**
* Walk the pointer deref chain. Always returns (zpVar, offset) such that one indirect
* access through zpVar at byte offset reaches the final thing:
* derefLast=true -> (zpVar),offset is the address of the value itself
* derefLast=false -> (zpVar),offset is the location of the field content
* This contract holds for every chain length: the final pointer (if any) is already
* followed inside deref(), so callers never need a second dereference.
* zpVar is P8ZP_SCRATCH_PTR for non-empty chains or non-zeropage pointers; it may be
* the original (zeropage) pointer variable only for an empty chain with derefLast=false.
* loadPointerFieldValue: for addOffsetToPointer=true, also load the value if the final
* field is a pointer-typed field (rather than returning its address).
*/
internal fun deref(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false, loadPointerFieldValue: Boolean=false): Pair<String, UByte> {
fun addFieldOffsetToScratchPointer(fieldoffset: UInt) {
if(fieldoffset==0u)
@@ -185,7 +285,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
if(addOffsetToPointer) {
asmgen.assignExpressionToVariable(pointer.startpointer, "P8ZP_SCRATCH_PTR", DataType.UWORD)
addFieldOffsetToScratchPointer(field.second.toUInt())
if(pointer.derefLast)
if(pointer.derefLast || (loadPointerFieldValue && field.first.isPointer))
updateScratchPointer()
return "P8ZP_SCRATCH_PTR" to 0u
} else {
@@ -193,6 +293,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
if(pointer.derefLast) {
asmgen.assignExpressionToVariable(pointer.startpointer, "P8ZP_SCRATCH_PTR", DataType.UWORD)
addFieldOffsetToScratchPointer(field.second.toUInt())
updateScratchPointer()
return "P8ZP_SCRATCH_PTR" to 0u
}
else
@@ -203,6 +304,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
asmgen.assignExpressionToVariable(pointer.startpointer, "P8ZP_SCRATCH_PTR", DataType.UWORD)
if(pointer.derefLast) {
addFieldOffsetToScratchPointer(field.second.toUInt())
updateScratchPointer()
return "P8ZP_SCRATCH_PTR" to 0u
}
else
@@ -235,22 +337,61 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
} else {
if(addOffsetToPointer) {
addFieldOffsetToScratchPointer(fieldinfo.second.toUInt())
if(loadPointerFieldValue && fieldinfo.first.isPointer)
updateScratchPointer()
return "P8ZP_SCRATCH_PTR" to 0u
} else
return "P8ZP_SCRATCH_PTR" to fieldinfo.second
}
}
internal fun assignPointerDerefExpression(target: AsmAssignTarget, value: PtPointerDeref) {
val (zpPtrVar, offset1) = deref(value)
val offset: UByte
if(value.derefLast) {
asmgen.loadIndirectWordAY(zpPtrVar, offset1)
asmgen.out(" sta $zpPtrVar | sty $zpPtrVar+1")
offset = 0.toUByte()
} else {
offset = offset1
internal fun indexedEffectiveAddress(target: IndexedPtrTarget): String {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
// generic: evaluate index as word, scale by element size, add field-corrected base pointer
asmgen.assignExpressionToVariable(target.index, "P8ZP_SCRATCH_W1", DataType.UWORD)
when (eltSize) {
1 -> {}
2 -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
4 -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
8 -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
else -> {
// generic multiply by eltSize
asmgen.out("""
lda P8ZP_SCRATCH_W1
ldy P8ZP_SCRATCH_W1+1
sta prog8_math.multiply_words.multiplier
sty prog8_math.multiply_words.multiplier+1
lda #<$eltSize
ldy #>$eltSize
jsr prog8_math.multiply_words
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1""")
}
}
// save scaled index to W2, compute base (field-corrected) into P8ZP_SCRATCH_PTR, then add
asmgen.out(" lda P8ZP_SCRATCH_W1 | sta P8ZP_SCRATCH_W2 | lda P8ZP_SCRATCH_W1+1 | sta P8ZP_SCRATCH_W2+1")
val (basePtr, _) = deref(target.pointer, addOffsetToPointer = true, loadPointerFieldValue = true)
if(basePtr != "P8ZP_SCRATCH_PTR") {
// deref returned the original zp pointer variable; copy it so we don't mutate it
asmgen.out("""
lda $basePtr
sta P8ZP_SCRATCH_PTR
lda $basePtr+1
sta P8ZP_SCRATCH_PTR+1""")
}
asmgen.out("""
clc
lda P8ZP_SCRATCH_PTR
adc P8ZP_SCRATCH_W2
sta P8ZP_SCRATCH_PTR
lda P8ZP_SCRATCH_PTR+1
adc P8ZP_SCRATCH_W2+1
sta P8ZP_SCRATCH_PTR+1""")
return "P8ZP_SCRATCH_PTR"
}
internal fun assignPointerDerefExpression(target: AsmAssignTarget, value: PtPointerDeref) {
val (zpPtrVar, offset) = deref(value)
if(value.type.isByteOrBool) {
asmgen.loadIndirectByte(zpPtrVar, offset)
asmgen.assignRegister(RegisterOrPair.A, target)
@@ -309,24 +450,24 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
internal fun inplaceModification(target: PtrTarget, operator: String, value: AsmAssignSource) {
when (operator) {
"+" -> {
if(target.dt.isByte && value.number?.number==1.0 || value.number?.number==2.0) {
if(target.dt.isByte && (value.number?.number==1.0 || value.number?.number==2.0)) {
val amount = value.number.number.toInt()
inplaceByteInc(target, amount)
} else {
if (target.dt.isByte) inplaceByteAdd(target, value)
else if (target.dt.isWord) inplaceWordAdd(target, value)
else if (target.dt.isWord || target.dt.isPointer) inplaceWordAdd(target, value)
else if (target.dt.isFloat) inplaceFloatAddOrMul(target, "FADD", value)
else if (target.dt.isLong) inplaceLongAdd(target, value)
else throw AssemblyError("weird dt ${target.dt} ${target.position}")
}
}
"-" -> {
if(target.dt.isByte && value.number?.number==1.0 || value.number?.number==2.0) {
if(target.dt.isByte && (value.number?.number==1.0 || value.number?.number==2.0)) {
val amount = value.number.number.toInt()
inplaceByteDec(target, amount)
} else {
if (target.dt.isByte) inplaceByteSub(target, value)
else if (target.dt.isWord) inplaceWordSub(target, value)
else if (target.dt.isWord || target.dt.isPointer) inplaceWordSub(target, value)
else if (target.dt.isFloat) inplaceFloatSubOrDiv(target, "FSUB", value)
else if (target.dt.isLong) inplaceLongSub(target, value)
else throw AssemblyError("weird dt ${target.position}")
@@ -409,392 +550,163 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
internal fun assignByte(target: IndexedPtrTarget, byte: Int) {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
val constIndex = target.index.asConstInteger()
if(constIndex!=null) {
val (zpPtrVar, offset2) = deref(target.pointer)
val offset = eltSize*constIndex + offset2.toInt()
if(offset>255) {
// need to add index to the pointer (but not clobber it) to allow index>255
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) { // TODO not very robust....
asmgen.out("""
lda $zpPtrVar+1
clc
adc #>$offset
sta $zpPtrVar+1
ldy #<$offset
lda #$byte
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
lda $zpPtrVar
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
clc
adc #>$offset
sta P8ZP_SCRATCH_PTR+1
ldy #<$offset
lda #$byte
sta (P8ZP_SCRATCH_PTR),y""")
}
} else {
asmgen.out("""
ldy #<$offset
lda #$byte
sta ($zpPtrVar),y""")
}
} else if(target.index is PtIdentifier) {
val (zpPtrVar, _) = deref(target.pointer, addOffsetToPointer=true)
val indexVarName = asmgen.asmVariableName(target.index)
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
// asmgen.loadScaledArrayIndexIntoRegister() ...
}
// element size is 1, can immediately add the index value
if(target.index.type.isWord) {
asmgen.out("""
clc
lda $zpPtrVar+1
adc $indexVarName+1
sta $zpPtrVar+1""")
}
asmgen.out("""
ldy $indexVarName
lda #$byte
sta ($zpPtrVar),y""")
} else {
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
// asmgen.loadScaledArrayIndexIntoRegister() ...
}
if(target.index.type.isByte) {
asmgen.pushCpuStack(BaseDataType.UBYTE, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.restoreRegisterStack(CpuRegister.Y, false)
asmgen.out(" lda #$byte | sta ($zpPtrVar),y")
}
else {
asmgen.pushCpuStack(BaseDataType.UWORD, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.out("""
pla
clc
adc $zpPtrVar+1
sta $zpPtrVar+1""")
if(asmgen.isTargetCpu(CpuType.CPU65C02)) asmgen.out(" ply") else asmgen.out(" pla | tay")
asmgen.out(" lda #$byte | sta ($zpPtrVar),y")
}
}
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectByte(byte, ptr, 0u)
}
internal fun assignWord(target: IndexedPtrTarget, word: Int) {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
val constIndex = target.index.asConstInteger()
if(constIndex!=null) {
val (zpPtrVar, offset2) = deref(target.pointer)
val offset = eltSize*constIndex + offset2.toInt()
if(offset>255) {
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) {
asmgen.out("""
lda $zpPtrVar+1
clc
adc #>$offset
sta $zpPtrVar+1
ldy #<$offset
lda #<$word
sta ($zpPtrVar),y
lda #>$word
iny
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
lda $zpPtrVar
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
clc
adc #>$offset
sta P8ZP_SCRATCH_PTR+1
ldy #<$offset
lda #<$word
sta (P8ZP_SCRATCH_PTR),y
lda #>$word
iny
sta (P8ZP_SCRATCH_PTR),y""")
}
} else {
asmgen.out("""
ldy #<$offset
lda #<$word
sta ($zpPtrVar),y
lda #>$word
iny
sta ($zpPtrVar),y""")
}
} else if(target.index is PtIdentifier) {
val (zpPtrVar, _) = deref(target.pointer, addOffsetToPointer=true)
val indexVarName = asmgen.asmVariableName(target.index)
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
}
if(target.index.type.isWord) {
asmgen.out("""
clc
lda $zpPtrVar+1
adc $indexVarName+1
sta $zpPtrVar+1""")
}
asmgen.out("""
ldy $indexVarName
lda #<$word
sta ($zpPtrVar),y
lda #>$word
iny
sta ($zpPtrVar),y""")
} else {
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
}
if(target.index.type.isByte) {
asmgen.pushCpuStack(BaseDataType.UBYTE, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.restoreRegisterStack(CpuRegister.Y, false)
asmgen.out("""
lda #<$word
sta ($zpPtrVar),y
lda #>$word
iny
sta ($zpPtrVar),y""")
} else {
asmgen.pushCpuStack(BaseDataType.UWORD, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.out("""
pla
clc
adc $zpPtrVar+1
sta $zpPtrVar+1""")
if(asmgen.isTargetCpu(CpuType.CPU65C02)) asmgen.out(" ply") else asmgen.out(" pla | tay")
asmgen.out("""
lda #<$word
sta ($zpPtrVar),y
lda #>$word
iny
sta ($zpPtrVar),y""")
}
}
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectWord(word, ptr, 0u)
}
internal fun assignLong(target: IndexedPtrTarget, long: Int) {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
val constIndex = target.index.asConstInteger()
if(constIndex!=null) {
val (zpPtrVar, offset2) = deref(target.pointer)
val offset = eltSize*constIndex + offset2.toInt()
val hex = long.toLongHex()
if(offset>255) {
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) {
asmgen.out("""
lda $zpPtrVar+1
clc
adc #>$offset
sta $zpPtrVar+1
ldy #<$offset
lda #$${hex.substring(6,8)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(4,6)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(2,4)}
sta ($zpPtrVar),y
iny
lda #$${hex.take(2)}
sta ($zpPtrVar),y""")
} else {
asmgen.out("""
lda $zpPtrVar
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
clc
adc #>$offset
sta P8ZP_SCRATCH_PTR+1
ldy #<$offset
lda #$${hex.substring(6,8)}
sta (P8ZP_SCRATCH_PTR),y
iny
lda #$${hex.substring(4,6)}
sta (P8ZP_SCRATCH_PTR),y
iny
lda #$${hex.substring(2,4)}
sta (P8ZP_SCRATCH_PTR),y
iny
lda #$${hex.take(2)}
sta (P8ZP_SCRATCH_PTR),y""")
}
} else {
asmgen.out("""
ldy #<$offset
lda #$${hex.substring(6,8)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(4,6)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(2,4)}
sta ($zpPtrVar),y
iny
lda #$${hex.take(2)}
sta ($zpPtrVar),y""")
}
} else if(target.index is PtIdentifier) {
val (zpPtrVar, _) = deref(target.pointer, addOffsetToPointer=true)
val indexVarName = asmgen.asmVariableName(target.index)
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
}
if(target.index.type.isWord) {
asmgen.out("""
clc
lda $zpPtrVar+1
adc $indexVarName+1
sta $zpPtrVar+1""")
}
val hex = long.toLongHex()
asmgen.out("""
ldy $indexVarName
lda #$${hex.substring(6,8)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(4,6)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(2,4)}
sta ($zpPtrVar),y
iny
lda #$${hex.take(2)}
sta ($zpPtrVar),y""")
} else {
if(eltSize!=1) {
TODO("multiply index by element size $eltSize ${target.position}")
}
val hex = long.toLongHex()
if(target.index.type.isByte) {
asmgen.pushCpuStack(BaseDataType.UBYTE, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.restoreRegisterStack(CpuRegister.Y, false)
asmgen.out("""
lda #$${hex.substring(6,8)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(4,6)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(2,4)}
sta ($zpPtrVar),y
iny
lda #$${hex.take(2)}
sta ($zpPtrVar),y""")
} else {
asmgen.pushCpuStack(BaseDataType.UWORD, target.index)
val (zpPtrVar, offset) = deref(target.pointer, addOffsetToPointer = true)
require(offset==0.toUByte())
asmgen.out("""
pla
clc
adc $zpPtrVar+1
sta $zpPtrVar+1""")
if(asmgen.isTargetCpu(CpuType.CPU65C02)) asmgen.out(" ply") else asmgen.out(" pla | tay")
asmgen.out("""
lda #$${hex.substring(6,8)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(4,6)}
sta ($zpPtrVar),y
iny
lda #$${hex.substring(2,4)}
sta ($zpPtrVar),y
iny
lda #$${hex.take(2)}
sta ($zpPtrVar),y""")
}
}
val ptr = indexedEffectiveAddress(target)
val hex = long.toLongHex()
asmgen.out("""
ldy #0
lda #$${hex.substring(6,8)}
sta ($ptr),y
iny
lda #$${hex.substring(4,6)}
sta ($ptr),y
iny
lda #$${hex.substring(2,4)}
sta ($ptr),y
iny
lda #$${hex.take(2)}
sta ($ptr),y""")
}
internal fun assignFloat(target: IndexedPtrTarget, float: Double) {
// For now, only the constant-index case is supported
val constIndex = target.index.asConstInteger()
if(constIndex!=null) {
val eltSize = asmgen.program.memsizer.memorySize(target.elementDt, null)
val (zpPtrVar, offset2) = deref(target.pointer)
val offset = eltSize*constIndex + offset2.toInt()
if(offset>255) {
if(zpPtrVar.startsWith("P8ZP_SCRATCH_")) {
asmgen.out("""
lda $zpPtrVar
clc
adc #<$offset
sta $zpPtrVar
lda $zpPtrVar+1
adc #>$offset
sta $zpPtrVar+1""")
asmgen.storeIndirectFloat(float, zpPtrVar, 0.toUByte())
} else {
asmgen.out("""
lda $zpPtrVar
clc
adc #<$offset
sta P8ZP_SCRATCH_PTR
lda $zpPtrVar+1
adc #>$offset
sta P8ZP_SCRATCH_PTR+1""")
asmgen.storeIndirectFloat(float, "P8ZP_SCRATCH_PTR", 0.toUByte())
}
} else {
asmgen.storeIndirectFloat(float, zpPtrVar, offset.toUByte())
}
} else {
TODO("array ptr assign const float with variable index not yet supported ${target.position}")
}
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectFloat(float, ptr, 0u)
}
internal fun assignFAC1(target: IndexedPtrTarget) {
TODO("array ptr assign FAC1 ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectFloatFP1(ptr, 0u)
}
internal fun assignFloatAY(target: IndexedPtrTarget) {
TODO("array ptr assign float AY ${target.position}")
// A/Y contains a pointer to the source float. Save it, compute destination address,
// restore source pointer into W1, load destination pointer into A/Y, and copy.
if(asmgen.isTargetCpu(CpuType.CPU65C02))
asmgen.out(" pha | phy")
else
asmgen.out(" pha | tya | pha")
val ptr = indexedEffectiveAddress(target)
if(asmgen.isTargetCpu(CpuType.CPU65C02)) {
asmgen.out("""
ply
pla
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
lda $ptr
ldy $ptr+1
jsr floats.copy_float""")
} else {
asmgen.out("""
pla
tay
pla
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1
lda $ptr
ldy $ptr+1
jsr floats.copy_float""")
}
}
internal fun assignFloatVar(target: IndexedPtrTarget, varName: String) {
TODO("array ptr assign float var ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectFloatVar(varName, ptr, 0u)
}
internal fun assignByteReg(target: IndexedPtrTarget, register: CpuRegister) {
// save register on the stack so indexedEffectiveAddress can freely use scratch regs
asmgen.saveRegisterStack(register, false)
TODO("array ptr assign byte reg ${target.position}")
asmgen.saveRegisterStack(register, false)
val ptr = indexedEffectiveAddress(target)
asmgen.restoreRegisterStack(register, false)
asmgen.storeIndirectByteReg(register, ptr, 0u, false, false)
}
internal fun assignWordReg(target: IndexedPtrTarget, regs: RegisterOrPair) {
// save word reg on the stack so indexedEffectiveAddress can freely use scratch regs
saveOnStack(regs)
TODO("array ptr assign word reg ${target.position}")
val ptr = indexedEffectiveAddress(target)
restoreFromStack(regs)
asmgen.storeIndirectWordReg(regs, ptr, 0u)
}
internal fun assignByteVar(target: IndexedPtrTarget, varName: String, extendToWord: Boolean, signed: Boolean) {
TODO("array ptr assign byte var ${target.position}")
val ptr = indexedEffectiveAddress(target)
if(extendToWord) {
// need to store byte var extended to word
asmgen.out(" lda $varName")
if(signed) asmgen.signExtendAXlsb(BaseDataType.BYTE) else asmgen.signExtendAXlsb(BaseDataType.UBYTE)
asmgen.storeIndirectWordReg(RegisterOrPair.AX, ptr, 0u)
} else {
asmgen.storeIndirectByteVar(varName, ptr, 0u)
}
}
internal fun assignWordVar(target: IndexedPtrTarget, varName: String) {
TODO("array ptr assign word var ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.storeIndirectWordVar(varName, DataType.UWORD, ptr, 0u)
}
internal fun assignLongVar(target: IndexedPtrTarget, varName: String) {
TODO("array ptr assign long var ${target.position}")
val ptr = indexedEffectiveAddress(target)
asmgen.out("""
ldy #0
lda $varName
sta ($ptr),y
iny
lda $varName+1
sta ($ptr),y
iny
lda $varName+2
sta ($ptr),y
iny
lda $varName+3
sta ($ptr),y""")
}
internal fun assignLongReg(target: IndexedPtrTarget, regs: RegisterOrPair) {
// generic fallback for long array via pointer
val startreg = regs.startregname()
// save source long on the stack in case index expression evaluation uses the same registers
asmgen.out("""
lda cx16.$startreg
pha
lda cx16.$startreg+1
pha
lda cx16.$startreg+2
pha
lda cx16.$startreg+3
pha""")
val ptr = indexedEffectiveAddress(target)
asmgen.out("""
pla
sta cx16.$startreg+3
pla
sta cx16.$startreg+2
pla
sta cx16.$startreg+1
pla
sta cx16.$startreg
ldy #0
lda cx16.$startreg
sta ($ptr),y
iny
lda cx16.$startreg+1
sta ($ptr),y
iny
lda cx16.$startreg+2
sta ($ptr),y
iny
lda cx16.$startreg+3
sta ($ptr),y""")
}
internal fun assignLongVar(pointer: PtPointerDeref, varName: String) {
+14
View File
@@ -160,6 +160,20 @@ class TestAsmOptimizer: FunSpec({
lines shouldBe listOf(" jsr floats.pushFAC", " rts", " nop", " nop")
}
test("optimizeJsrRts: three consecutive jsr only tail jsr+rts becomes jmp") {
val lines = mutableListOf(" jsr \$4000", " jsr \$5000", " jsr \$6000", " rts", " nop", " nop")
optimize(lines)
lines shouldBe listOf(" jsr \$4000", " jsr \$5000", " jmp \$6000", " nop", " nop")
}
test("optimizeJsrRts: single line with embedded newlines only tail converted defensively") {
val lines = mutableListOf(" jsr \$4000\n jsr \$5000\n jsr \$6000", " rts", " nop", " nop")
optimize(lines)
// Before fix this became jmp/jmp/jmp via replace("jsr","jmp"); after fix only one jmp via replaceMnemonic
val jmpCount = lines.joinToString("\n").split("jmp").size - 1
jmpCount shouldBe 1
}
// --- optimizeUselessPushPopStack ---
test("optimizeUselessPushPop: removes phy+ldy+pla") {
@@ -224,6 +224,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
"or=" -> {
val shortcutLabel = codeGen.createLabelName()
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
addInstr(inplaceInstrs, IRInstruction(Opcode.CMPI, targetDt, reg1 = oldvalueReg, immediate = 0), null)
addInstr(inplaceInstrs, IRInstruction(Opcode.BSTNE, labelSymbol = shortcutLabel), null)
val valueTr = exprGen.translateExpression(value)
inplaceInstrs += valueTr.chunks
@@ -232,6 +234,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
}
"and=" -> {
val shortcutLabel = codeGen.createLabelName()
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
addInstr(inplaceInstrs, IRInstruction(Opcode.CMPI, targetDt, reg1 = oldvalueReg, immediate = 0), null)
addInstr(inplaceInstrs, IRInstruction(Opcode.BSTEQ, labelSymbol = shortcutLabel), null)
val valueTr = exprGen.translateExpression(value)
inplaceInstrs += valueTr.chunks
@@ -249,6 +253,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
codeGen.storeValueAtPointersLocation(inplaceInstrs, addressReg, fieldOffset, pointerDeref.type, false, oldvalueReg)
chunks = inplaceInstrs
} else if(array?.pointerderef != null) {
chunks = fallbackAssign(augAssign)
} else {
chunks = when (augAssign.operator) {
"+=" -> operatorPlusInplace(symbol, array, constAddress, memTarget, targetDt, value)
@@ -385,7 +391,11 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
val arrayVariableName = array.variable!!.name
val skipCarryLabel = codeGen.createLabelName()
if(constIndex!=null) {
val negLsbReg = codeGen.registers.next(IRDataType.BYTE)
addInstr(result, IRInstruction(Opcode.NEGM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex), null)
addInstr(result, IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = negLsbReg, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex), null)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = negLsbReg, immediate = 0), null)
addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = skipCarryLabel), null)
addInstr(result, IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex), null)
addInstr(result, IRInstruction(Opcode.NEGM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex), skipCarryLabel)
@@ -1029,6 +1039,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, address = constAddress.toAddress())
else
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, labelSymbol = symbol)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, vmDt, reg1=inplaceReg, immediate = 0)
it += IRInstruction(Opcode.BSTEQ, labelSymbol = shortcutLabel)
}
addToResult(result, tr, tr.resultReg, -1)
@@ -1289,6 +1301,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, address = constAddress.toAddress())
else
IRInstruction(Opcode.LOADM, vmDt, reg1=inplaceReg, labelSymbol = symbol)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, vmDt, reg1=inplaceReg, immediate = 0)
it += IRInstruction(Opcode.BSTNE, labelSymbol = shortcutLabel)
}
addToResult(result, tr, tr.resultReg, -1)
@@ -1716,6 +1730,8 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
val lsbReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = lsbReg, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = lsbReg, immediate = 0)
it += IRInstruction(Opcode.BSTNE, labelSymbol = skip)
it += IRInstruction(Opcode.DECM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex)
}
@@ -1968,8 +1984,12 @@ internal class AssignmentGen(private val codeGen: IRCodeGen, private val exprGen
if(constValue==1) {
val skip = codeGen.createLabelName()
val lsbReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex)
it += IRInstruction(Opcode.LOADM, IRDataType.BYTE, reg1 = lsbReg, labelSymbol = arrayVariableName+"_lsb", symbolOffset = constIndex)
if(!codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps)
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = lsbReg, immediate = 0)
it += IRInstruction(Opcode.BSTNE, labelSymbol = skip)
it += IRInstruction(Opcode.INCM, IRDataType.BYTE, labelSymbol = arrayVariableName+"_msb", symbolOffset = constIndex)
}
@@ -120,21 +120,21 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.WORD)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.WORD, reg1=resultRegister), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
return ExpressionCodeResult(result, IRDataType.WORD, resultRegister, -1)
}
private fun funcPopL(): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.LONG)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.LONG, reg1=resultRegister), null)
return ExpressionCodeResult(result, IRDataType.BYTE, resultRegister, -1)
return ExpressionCodeResult(result, IRDataType.LONG, resultRegister, -1)
}
private fun funcPopF(): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
val resultRegister = codeGen.registers.next(IRDataType.FLOAT)
addInstr(result, IRInstruction(Opcode.POP, IRDataType.FLOAT, fpReg1 = RegisterNum(resultRegister)), null)
return ExpressionCodeResult(result, IRDataType.BYTE, -1, resultRegister)
return ExpressionCodeResult(result, IRDataType.FLOAT, -1, resultRegister)
}
private fun funcCopyFromPointer1ToPointer2(call: PtFunctionCall, type: IRDataType): ExpressionCodeResult {
@@ -300,12 +300,15 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val tr = exprGen.translateExpression(call.args[0])
addToResult(result, tr, tr.resultReg, -1)
val needsExplicitCmpi = !codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
when (sourceDt.base) {
BaseDataType.BYTE -> {
val notNegativeLabel = codeGen.createLabelName()
val compareReg = codeGen.registers.next(IRDataType.BYTE)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.BYTE, reg1=compareReg, reg2=tr.resultReg)
if(needsExplicitCmpi)
it += IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=compareReg, immediate = 0)
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
it += IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=tr.resultReg)
}
@@ -317,6 +320,8 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val compareReg = codeGen.registers.next(IRDataType.WORD)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.WORD, reg1=compareReg, reg2=tr.resultReg)
if(needsExplicitCmpi)
it += IRInstruction(Opcode.CMPI, IRDataType.WORD, reg1=compareReg, immediate = 0)
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
it += IRInstruction(Opcode.NEG, IRDataType.WORD, reg1=tr.resultReg)
}
@@ -328,6 +333,8 @@ internal class BuiltinFuncGen(private val codeGen: IRCodeGen, private val exprGe
val compareReg = codeGen.registers.next(IRDataType.LONG)
result += IRCodeChunk(null, null).also {
it += IRInstruction(Opcode.LOADR, IRDataType.LONG, reg1=compareReg, reg2=tr.resultReg)
if(needsExplicitCmpi)
it += IRInstruction(Opcode.CMPI, IRDataType.LONG, reg1=compareReg, immediate = 0)
it += IRInstruction(Opcode.BSTPOS, labelSymbol = notNegativeLabel)
it += IRInstruction(Opcode.NEG, IRDataType.LONG, reg1=tr.resultReg)
}
@@ -22,6 +22,21 @@ internal class ExpressionCodeResult(val chunks: IRCodeChunks, val dt: IRDataType
}
}
/**
* True if the explicit CMPI #0 that would test a translated result value against zero can be
* skipped, because the last emitted instruction of that translation already sets the status
* bits. This is only allowed on targets that honor the "multi-byte ops set status bits based
* on the full value" contract (see CpuType.statusBitsOnMultiByteOps): on 8-bit targets,
* multi-byte ops may only set Z from the last byte, so the explicit CMPI is required there.
*
* Note: when the CMPI is skipped, a following branch tests Z meaning "the compared value is
* zero". That is only meaningful when the last instruction was itself a CMP/CMPI/SGN/BITTST
* (what OpcodesThatSetStatusbits guarantees). A future target that honors the contract while
* setting status bits with other instructions must re-check this assumption.
*/
internal fun CpuType.canSkipExplicitCmpi(lastInstr: IRInstruction?): Boolean =
statusBitsOnMultiByteOps && lastInstr != null && lastInstr.opcode in OpcodesThatSetStatusbits
internal class ExpressionGen(private val codeGen: IRCodeGen) {
fun translateExpression(expr: PtExpression): ExpressionCodeResult {
@@ -268,14 +283,8 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
// Use status flags from condition expression when possible, avoiding redundant CMPI #0
val condTr = translateExpression(ifExpr.condition)
addToResult(result, condTr, condTr.resultReg, -1)
// Only skip the CMPI if the target honors the "multi-byte ops set Z based on full
// value" contract. On 8-bit targets (6502/65C02) this is false, so we always emit
// an explicit CMPI - a multi-byte op may have set Z based on only the last byte.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val lastInstr = condTr.chunks.lastOrNull()?.instructions?.lastOrNull()
val skipCmpi = codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
val skipCmpi = codeGen.options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1=condTr.resultReg, immediate = 0), null)
}
@@ -1316,14 +1325,8 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1)
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
// Only skip the CMPI if the target honors the "multi-byte ops set Z
// based on full value" contract. On 8-bit targets a previous op like
// DEC only set Z from the last byte, so the explicit CMPI is required.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val skipCmpi = rightConst.toInt() == 0
&& codeGen.options.compTarget.cpu.statusBitsOnMultiByteOps
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
&& codeGen.options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, leftTr.dt, reg1 = leftTr.resultReg, immediate = rightConst.toInt()), null)
}
@@ -1450,6 +1453,25 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
}
}
/**
* Emit the branch that decides if the short-circuit evaluation of a boolean `or`/`and` has to
* skip its right hand side: [branchWhenZeroTrue] selects if the branch is taken when the left
* hand side value in the register is zero (false) instead of when it is non-zero (true).
* The status bits have to be set from that register first, because only CMP/CMPI/SGN/BITTST do
* that on targets that don't honor CpuType.statusBitsOnMultiByteOps, and the value can also come
* straight from a load or a register move. [canSkipExplicitCmpi] decides if setting them can be
* skipped on this target.
*/
private fun addShortCircuitBranch(result: MutableList<IRCodeChunkBase>, leftTr: ExpressionCodeResult, shortcutLabel: String, branchWhenZeroTrue: Boolean) {
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
val skipCmpi = codeGen.options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, IRDataType.BYTE, reg1 = leftTr.resultReg, immediate = 0), null)
}
val branchOpcode = if (branchWhenZeroTrue) Opcode.BSTEQ else Opcode.BSTNE
addInstr(result, IRInstruction(branchOpcode, labelSymbol = shortcutLabel), null)
}
private fun operatorAnd(binExpr: PtBinaryExpression, vmDt: IRDataType, bitwise: Boolean): ExpressionCodeResult {
val result = mutableListOf<IRCodeChunkBase>()
if(!bitwise && !binExpr.right.isSimple()) {
@@ -1457,7 +1479,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1)
val shortcutLabel = codeGen.createLabelName()
addInstr(result, IRInstruction(Opcode.BSTEQ, labelSymbol = shortcutLabel), null)
addShortCircuitBranch(result, leftTr, shortcutLabel, branchWhenZeroTrue = true)
val rightTr = translateExpression(binExpr.right)
addToResult(result, rightTr, leftTr.resultReg, -1)
result += IRCodeChunk(shortcutLabel, null)
@@ -1491,7 +1513,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
val leftTr = translateExpression(binExpr.left)
addToResult(result, leftTr, leftTr.resultReg, -1)
val shortcutLabel = codeGen.createLabelName()
addInstr(result, IRInstruction(Opcode.BSTNE, labelSymbol = shortcutLabel), null)
addShortCircuitBranch(result, leftTr, shortcutLabel, branchWhenZeroTrue = false)
val rightTr = translateExpression(binExpr.right)
addToResult(result, rightTr, leftTr.resultReg, -1)
result += IRCodeChunk(shortcutLabel, null)
@@ -888,7 +888,10 @@ class IRCodeGen(
val chunk2 = addConstMem(loopvarDtIr, null, loopvarSymbol, iterable.step)
if(loopvarDtIr==IRDataType.BYTE && iterable.step==-1 && iterable.last==0) {
// downto 0 optimization (byte)
if(loopvarDt.isSignedByte || iterable.first<=127) {
// Only rely on DECM setting status bits on targets where it actually does (6502 hardware,
// or targets that honor the strict multi-byte status-bits contract). On the VM DECM
// leaves flags untouched, so BSTPOS would read stale state and loop forever.
if((loopvarDt.isSignedByte || iterable.first<=127) && (options.compTarget.cpu.is6502 || options.compTarget.cpu.statusBitsOnMultiByteOps)) {
chunk2 += IRInstruction(Opcode.BSTPOS, labelSymbol = loopLabel)
} else {
chunk2 += IRInstruction(Opcode.LOADM, loopvarDtIr, reg1 = indexReg, labelSymbol = loopvarSymbol)
@@ -1336,17 +1339,8 @@ class IRCodeGen(
// Fallback: materialize expression and branch on result
val tr = expressionEval.translateExpression(condition)
result += tr.chunks
// Only skip the CMPI #0 if BOTH:
// 1) The previous instruction is documented to set status bits (Z/N), AND
// 2) The target CPU honors the "multi-byte ops set Z based on full value" contract
// (see CpuType.statusBitsOnMultiByteOps). For 8-bit targets this contract
// is false: e.g. a 16-bit DEC only sets Z from the low byte's dec, not the
// full 16-bit value, so we MUST emit an explicit CMPI here.
val lastInstr = tr.chunks.lastOrNull()?.instructions?.lastOrNull()
val targetHonorsContract = options.compTarget.cpu.statusBitsOnMultiByteOps
val skipCmpi = targetHonorsContract
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
val skipCmpi = options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
if (!skipCmpi) {
addInstr(result, IRInstruction(Opcode.CMPI, tr.dt, reg1 = tr.resultReg, immediate = 0), null)
}
@@ -1389,15 +1383,8 @@ class IRCodeGen(
if (number != null) {
val isComparingWithZero = number == 0
val lastInstr = leftTr.chunks.lastOrNull()?.instructions?.lastOrNull()
// Skip the CMPI only if the target honors the "multi-byte ops set Z correctly"
// contract. On 8-bit targets a 16/32-bit op like DEC only sets Z from the last
// byte, so we cannot rely on the previous instruction's status bits - the
// CMPI is required to test the full multi-byte value.
// See CpuType.statusBitsOnMultiByteOps for details.
val canSkipCmpi = isComparingWithZero
&& options.compTarget.cpu.statusBitsOnMultiByteOps
&& lastInstr != null
&& lastInstr.opcode in OpcodesThatSetStatusbits
&& options.compTarget.cpu.canSkipExplicitCmpi(lastInstr)
if ((onTrueLabel != null || onTrueAddress != null) && (onFalseLabel == null && onFalseAddress == null)) {
var (opcode, useCmpi) = getIntegerComparisonBranch(condition.operator, false, signed)
@@ -67,50 +67,48 @@ class ConstExprEvaluator {
}
private fun bitwiseOr(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(right.type.isIntegerOrBool) {
val leftDt = left.type
if(leftDt.isByteOrBool)
return NumericLiteral(BaseDataType.UBYTE, ((left.number.toInt() or right.number.toInt()) and 255).toDouble(), left.position)
else if(leftDt.isInteger) {
if (leftDt == BaseDataType.UWORD || leftDt == BaseDataType.WORD)
return NumericLiteral(BaseDataType.UWORD, ((left.number.toInt() or right.number.toInt()) and 65535).toDouble(), left.position)
else if (leftDt == BaseDataType.LONG)
return NumericLiteral(BaseDataType.LONG, (left.number.toInt() or right.number.toInt()).toDouble(), left.position)
}
if(left.type.isIntegerOrBool && right.type.isIntegerOrBool) {
val resultType = bitwiseResultType(left.type, right.type)
val result = (left.number.toInt() or right.number.toInt()) and bitwiseMask(resultType)
return NumericLiteral(resultType, result.toDouble(), left.position)
}
throw ExpressionError("cannot calculate $left | $right", left.position)
}
private fun bitwiseAnd(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(right.type.isIntegerOrBool) {
val leftDt = left.type
if(leftDt.isByteOrBool)
return NumericLiteral(BaseDataType.UBYTE, ((left.number.toInt() and right.number.toInt()) and 255).toDouble(), left.position)
else if(leftDt.isInteger) {
if (leftDt == BaseDataType.UWORD || leftDt == BaseDataType.WORD)
return NumericLiteral(BaseDataType.UWORD, ((left.number.toInt() and right.number.toInt()) and 65535).toDouble(), left.position)
else if (leftDt == BaseDataType.LONG)
return NumericLiteral(BaseDataType.LONG, (left.number.toInt() and right.number.toInt()).toDouble(), left.position)
}
if(left.type.isIntegerOrBool && right.type.isIntegerOrBool) {
val resultType = bitwiseResultType(left.type, right.type)
val result = (left.number.toInt() and right.number.toInt()) and bitwiseMask(resultType)
return NumericLiteral(resultType, result.toDouble(), left.position)
}
throw ExpressionError("cannot calculate $left & $right", left.position)
}
private fun bitwiseXor(left: NumericLiteral, right: NumericLiteral): NumericLiteral {
if(right.type.isIntegerOrBool) {
val leftDt = left.type
if(leftDt.isByteOrBool)
return NumericLiteral(BaseDataType.UBYTE, ((left.number.toInt() xor right.number.toInt()) and 255).toDouble(), left.position)
else if(leftDt.isInteger) {
if (leftDt == BaseDataType.UWORD || leftDt == BaseDataType.WORD)
return NumericLiteral(BaseDataType.UWORD, ((left.number.toInt() xor right.number.toInt()) and 65535).toDouble(), left.position)
else if (leftDt == BaseDataType.LONG)
return NumericLiteral(BaseDataType.LONG, (left.number.toInt() xor right.number.toInt()).toDouble(), left.position)
}
if(left.type.isIntegerOrBool && right.type.isIntegerOrBool) {
val resultType = bitwiseResultType(left.type, right.type)
val result = (left.number.toInt() xor right.number.toInt()) and bitwiseMask(resultType)
return NumericLiteral(resultType, result.toDouble(), left.position)
}
throw ExpressionError("cannot calculate $left ^ $right", left.position)
}
private fun bitwiseResultType(type1: BaseDataType, type2: BaseDataType): BaseDataType {
val width = if (type1.largerSizeThan(type2)) type1 else type2
return when {
width.isByteOrBool -> BaseDataType.UBYTE
width.isWord -> BaseDataType.UWORD
width == BaseDataType.LONG -> BaseDataType.LONG
else -> BaseDataType.UBYTE
}
}
private fun bitwiseMask(type: BaseDataType): Int = when(type) {
BaseDataType.UBYTE, BaseDataType.BYTE -> 0xFF
BaseDataType.UWORD, BaseDataType.WORD -> 0xFFFF
else -> 0xFFFFFFFF.toInt()
}
private fun logicalAnd(left: NumericLiteral, right: NumericLiteral): NumericLiteral =
NumericLiteral.fromBoolean(left.asBooleanValue and right.asBooleanValue, left.position)
@@ -59,7 +59,7 @@ class VarConstantValueTypeAdjuster(
if (decl.value != null && !isConstantInitializer(decl.value!!))
return noModifications
if (decl.value != null && isMemorySlabInit(decl.value!!))
return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferrably in zeropage)
return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferably in zeropage)
val usages = callGraph.usages(decl)
val (writes, reads) = usages
.partition {
+41 -23
View File
@@ -11,6 +11,8 @@ import prog8.code.target.VMTarget
private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.values.isEmpty()
private class CannotInlineException : Exception()
/**
* The Inliner performs subroutine inlining on the AST.
*
@@ -87,6 +89,13 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
fun isBodyInlineable(stmt: Assignment): Boolean {
// Never auto-inline a sub that assigns directly to one of its parameters.
// Substituting the call argument into the assignment target would either
// crash (literal argument has no identifier to assign to) or change semantics
// (assigning to the caller's variable instead of the local parameter copy).
val targetName = stmt.target.identifier?.nameInSource?.lastOrNull()
if (targetName != null && subroutine.parameters.any { it.name == targetName })
return false
return if (stmt.value.isSimple) {
val targetInline =
if (stmt.target.identifier != null) {
@@ -139,7 +148,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
stmt.target is NumericLiteral
}
if (!subroutine.isAsmSubroutine && subroutine.parameters.size <= 1) {
if (!subroutine.isAsmSubroutine && subroutine.parameters.size <= 1 && subroutine.parameters.all { it.registerOrPair == null }) {
val containsSubsOrVariables = subroutine.statements.any { it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM) }
if (!containsSubsOrVariables) {
val functionalStatements = subroutine.statements.filter { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
@@ -254,11 +263,23 @@ class Inliner(private val program: Program, private val options: CompilationOpti
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
}
if (sub.parameters.any { it.registerOrPair != null }) {
return false to "subroutines with register-mapped parameters cannot be inlined"
}
if (sub.parameters.size == 1) {
val arg = fcall.args.first()
if (!arg.isSimple) {
return false to "argument is too complex to inline (might have side effects)"
}
// Refuse to inline a sub that assigns directly to its parameter.
// Substituting the argument into the target would crash for a literal
// argument, or wrongly modify the caller's variable for a variable argument
// (parameters are passed by value, so the sub only modifies its local copy).
val paramName = sub.parameters[0].name
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
if ((bodyStmt as? Assignment)?.target?.identifier?.nameInSource?.lastOrNull() == paramName)
return false to "cannot inline subroutine that assigns to its parameter"
}
if (options.compTarget.name != VMTarget.NAME) {
@@ -304,9 +325,9 @@ class Inliner(private val program: Program, private val options: CompilationOpti
fun inlineFunctionBody(toInline: Return): Iterable<AstModification> {
// call site is an expression, so we have to have a Return here in the inlined sub to provide the values
return if (toInline.values.size == 1 && functionCallExpr !== toInline.values[0]) {
logInlining(sub, functionCallExpr)
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as? Expression
?: return noModifications
sub.hasBeenInlined = true
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as Expression
listOf(AstReplaceNode(functionCallExpr, substitutedReturn, parent))
} else
noModifications
@@ -327,18 +348,17 @@ class Inliner(private val program: Program, private val options: CompilationOpti
fun possiblyShortCircuitFunctionCall(toInline: Return): Iterable<AstModification> {
// Substitute parameters in the return values first
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as Return
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as? Return
?: return noModifications
val functionCalls = substitutedReturn.values.filterIsInstance<FunctionCallExpression>()
if (functionCalls.isEmpty()) {
// No function calls in the return values - the void call has no side effects and can be removed.
logInlining(sub, origNode)
sub.hasBeenInlined = true
return listOf(AstRemove(origNode as Statement, parent as IStatementContainer))
}
// There are function calls in the return values - convert each to a void statement.
logInlining(sub, origNode)
sub.hasBeenInlined = true
val voidStatements: MutableList<Statement> = functionCalls.map { fcall ->
@@ -356,10 +376,10 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
fun possiblyInlineFunctionBody(toInline: Statement): Iterable<AstModification> {
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as Statement
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as? Statement
?: return noModifications
return if (origNode !== toInline) {
logInlining(sub, origNode)
sub.hasBeenInlined = true
listOf(AstReplaceNode(origNode, inlinedStatement, parent))
} else
@@ -391,13 +411,6 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
private fun logInlining(sub: Subroutine, call: Node) {
// TODO remove this logging once we are sure everything works good
if (sub.parameters.isNotEmpty() && !options.quiet) {
println(">>> INLINER: inlining '${sub.name}' at ${call.position} (subroutine at ${sub.position})")
}
}
private fun isSimpleReturnExpression(expr: Expression): Boolean {
return when (expr) {
is NumericLiteral -> true
@@ -436,7 +449,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
return noModifications
// Substitute parameters in all return values
val substitutedReturn = substituteParameters(sub, fcall, toInline) as Return
val substitutedReturn = substituteParameters(sub, fcall, toInline) as? Return
?: return noModifications
// Create multiple single assignments, skipping void targets (they discard the value)
val newAssignments = multiTargets.zip(substitutedReturn.values)
@@ -463,7 +477,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
return listOf(AstReplaceNode(assignment, scope, parent))
}
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node {
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node? {
val paramVarDecls = sub.parameters.map { param ->
sub.statements.filterIsInstance<VarDecl>().find { it.origin == VarDeclOrigin.SUBROUTINEPARAM && it.name == param.name }
?: throw FatalAstException("parameter ${param.name} not found in subroutine ${sub.name}")
@@ -486,7 +500,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
is ArrayIndexedExpression -> ArrayIndexedExpression(
n.plainarrayvar?.let {
val subId = substitute(it)
if (subId !is IdentifierReference) throw FatalAstException("parameter substituted by ${subId::class.simpleName} but an identifier reference was expected")
if (subId !is IdentifierReference) throw CannotInlineException()
subId
},
n.nestedArray?.let { substitute(it) as ArrayIndexedExpression },
@@ -500,7 +514,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
val newTarget = n.target.copy()
newTarget.identifier = n.target.identifier?.let {
val subId = substitute(it)
if (subId !is IdentifierReference) throw FatalAstException("parameter substituted by ${subId::class.simpleName} but an identifier reference was expected")
if (subId !is IdentifierReference) throw CannotInlineException()
subId
}
newTarget.memoryAddress = n.target.memoryAddress?.let { DirectMemoryWrite(substitute(it.addressExpression) as Expression, it.position) }
@@ -523,7 +537,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
is FunctionCallExpression -> FunctionCallExpression(
substitute(n.target).let {
if (it !is IdentifierReference) throw FatalAstException("parameter substituted by ${it::class.simpleName} but an identifier reference was expected")
if (it !is IdentifierReference) throw CannotInlineException()
it
},
n.args.map { substitute(it) as Expression }.toMutableList(),
@@ -531,7 +545,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
)
is FunctionCallStatement -> FunctionCallStatement(
substitute(n.target).let {
if (it !is IdentifierReference) throw FatalAstException("parameter substituted by ${it::class.simpleName} but an identifier reference was expected")
if (it !is IdentifierReference) throw CannotInlineException()
it
},
n.args.map { substitute(it) as Expression }.toMutableList(),
@@ -547,7 +561,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
is AddressOf -> AddressOf(
n.identifier?.let {
val subId = substitute(it)
if (subId !is IdentifierReference) throw FatalAstException("parameter substituted by ${subId::class.simpleName} but an identifier reference was expected")
if (subId !is IdentifierReference) throw CannotInlineException()
subId
},
n.arrayIndex?.let { substitute(it) as ArrayIndex },
@@ -560,6 +574,10 @@ class Inliner(private val program: Program, private val options: CompilationOpti
}
}
return substitute(node)
return try {
substitute(node)
} catch (_: CannotInlineException) {
null
}
}
}
@@ -494,15 +494,19 @@ class StatementOptimizer(private val program: Program,
return listOf(AstReplaceNode(assignment, ifstmt, parent))
}
if(v2 is NumericLiteral || v2 is IdentifierReference) {
if(v2 is NumericLiteral) {
// x = min(expression, 100) -> x=expression, if x>100 x=100
// Only safe with a real constant: the assignment to x happens before the
// comparison, so comparing against anything that x could alias (such as a
// variable that happens to be the assignment target) would read the already
// overwritten value.
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v2, assignment.target, assignment.position)
return listOf(
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
AstReplaceNode(assignment, assign, parent)
)
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
} else if(v1 is NumericLiteral) {
// x = min(100, expression) -> x=expression, if x>100 x=100
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), ">", v1, assignment.target, assignment.position)
@@ -527,15 +531,17 @@ class StatementOptimizer(private val program: Program,
return listOf(AstReplaceNode(assignment, ifstmt, parent))
}
if(v2 is NumericLiteral || v2 is IdentifierReference) {
if(v2 is NumericLiteral) {
// x = max(expression, 100) -> x=expression, if x<100 x=100
// See the note in the min__ case above: the compared operand must be a
// real constant, never a variable that the assignment could have aliased.
val assign = Assignment(assignment.target.copy(), v1, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v2, assignment.target, assignment.position)
return listOf(
AstInsert.after(assignment, ifstmt, parent as IStatementContainer),
AstReplaceNode(assignment, assign, parent)
)
} else if(v1 is NumericLiteral || v1 is IdentifierReference) {
} else if(v1 is NumericLiteral) {
// x = max(100, expression) -> x=expression, if x<100 x=100
val assign = Assignment(assignment.target.copy(), v2, AssignmentOrigin.OPTIMIZER, assignment.position)
val ifstmt = makeMinMaxCheckAndAssignRight(assign.target.toExpression(), "<", v1, assignment.target, assignment.position)
+19 -4
View File
@@ -349,16 +349,28 @@ hline_filled_right .byte 0, %10000000, %11000000, %11100000, %11110000, %1111
sub disc(uword xcenter, ubyte ycenter, ubyte radius) {
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
; Midpoint algorithm, filled.
; Overdraws horizontal lines unfortunately.
if radius==0
return
ubyte @zp yy = 0
word decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*2+1)
horizontal_line(xcenter-radius, ycenter-yy, radius*2+1)
horizontal_line(xcenter-yy, ycenter+radius, yy*2+1)
horizontal_line(xcenter-yy, ycenter-radius, yy*2+1)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*2+1)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*2+1)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -367,6 +379,10 @@ hline_filled_right .byte 0, %10000000, %11000000, %11100000, %11110000, %1111
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*2+1)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*2+1)
}
}
@@ -446,4 +462,3 @@ _y_lookup_hi .byte >_plot_y_values
}
+38 -4
View File
@@ -439,12 +439,25 @@ gfx_hires {
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, color)
horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, color)
horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, color)
horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, color)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -453,6 +466,10 @@ gfx_hires {
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
}
sub safe_disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, ubyte color) {
@@ -462,12 +479,25 @@ gfx_hires {
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
safe_horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, color)
safe_horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, color)
safe_horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, color)
safe_horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, color)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
safe_horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
safe_horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -476,6 +506,10 @@ gfx_hires {
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
safe_horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
safe_horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
}
sub plot(uword @zp xx, uword @zp yy, ubyte @zp color) {
+46 -8
View File
@@ -501,11 +501,24 @@ gfx_lores {
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, color)
horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, color)
horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, color)
horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, color)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -514,6 +527,10 @@ gfx_lores {
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, color)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, color)
}
}
sub safe_disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, ubyte color) {
@@ -523,6 +540,9 @@ gfx_lores {
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
uword liney = ycenter+yy
@@ -532,12 +552,22 @@ gfx_lores {
if msb(liney)==0
safe_horizontal_line(xcenter-radius, lsb(liney), radius*$0002+1, color)
liney = ycenter+radius
if msb(liney)==0
safe_horizontal_line(xcenter-yy, lsb(liney), yy*$0002+1, color)
liney = ycenter-radius
if msb(liney)==0
safe_horizontal_line(xcenter-yy, lsb(liney), yy*$0002+1, color)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
liney = ycenter+pendingRadius
if msb(liney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(liney), pendingWidth*$0002+1, color)
liney = ycenter-pendingRadius
if msb(liney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(liney), pendingWidth*$0002+1, color)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
@@ -547,6 +577,14 @@ gfx_lores {
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
uword flushLiney = ycenter+pendingRadius
if msb(flushLiney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(flushLiney), pendingWidth*$0002+1, color)
flushLiney = ycenter-pendingRadius
if msb(flushLiney)==0
safe_horizontal_line(xcenter-pendingWidth, lsb(flushLiney), pendingWidth*$0002+1, color)
}
}
asmsub plot(uword x @AX, ubyte y @Y, ubyte color @R0) {
+38 -6
View File
@@ -604,17 +604,29 @@ drawmode: ora cx16.r15L
sub disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, bool draw) {
; Warning: NO BOUNDS CHECKS. Make sure circle fits in the screen.
; Midpoint algorithm, filled
; Note: has problems with INVERT draw mode because of horizontal span overdrawing. Horizontal lines may occur.
if radius==0
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, draw)
horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, draw)
horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, draw)
horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, draw)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -623,22 +635,38 @@ drawmode: ora cx16.r15L
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
}
sub safe_disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, bool draw) {
; Does bounds checking and clipping.
; Midpoint algorithm, filled
; Note: has problems with INVERT draw mode because of horizontal span overdrawing. Horizontal lines may occur.
if radius==0
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
safe_horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, draw)
safe_horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, draw)
safe_horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, draw)
safe_horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, draw)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
safe_horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
safe_horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -647,6 +675,10 @@ drawmode: ora cx16.r15L
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
safe_horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
safe_horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
}
sub plot(uword @zp xx, uword @zp yy, bool @zp draw) {
+2 -2
View File
@@ -1744,8 +1744,8 @@ asmsub restore_irq() clobbers(A) {
sta cx16.via1ier
; set timer1 back to free running mode
lda cx16.via1acr
and %00111111
ora %01000000
and #%00111111
ora #%01000000
sta cx16.via1acr
lda #$ff
sta cx16.via1t1ll
+38 -4
View File
@@ -304,11 +304,24 @@ monogfx {
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, draw)
horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, draw)
horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, draw)
horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, draw)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -317,6 +330,10 @@ monogfx {
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
}
sub safe_disc(uword @zp xcenter, uword @zp ycenter, ubyte @zp radius, bool draw) {
@@ -326,11 +343,24 @@ monogfx {
return
ubyte @zp yy = 0
word @zp decisionOver2 = (1 as word)-radius
ubyte pendingRadius
ubyte pendingWidth
bool hasPending = false
while radius>=yy {
safe_horizontal_line(xcenter-radius, ycenter+yy, radius*$0002+1, draw)
safe_horizontal_line(xcenter-radius, ycenter-yy, radius*$0002+1, draw)
safe_horizontal_line(xcenter-yy, ycenter+radius, yy*$0002+1, draw)
safe_horizontal_line(xcenter-yy, ycenter-radius, yy*$0002+1, draw)
if hasPending and pendingRadius != radius {
if pendingRadius != pendingWidth {
safe_horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
safe_horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
hasPending = false
}
if not hasPending {
pendingRadius = radius
hasPending = true
}
pendingWidth = yy
yy++
if decisionOver2>=0 {
radius--
@@ -339,6 +369,10 @@ monogfx {
decisionOver2 += yy*$0002
decisionOver2++
}
if hasPending and pendingRadius != pendingWidth {
safe_horizontal_line(xcenter-pendingWidth, ycenter+pendingRadius, pendingWidth*$0002+1, draw)
safe_horizontal_line(xcenter-pendingWidth, ycenter-pendingRadius, pendingWidth*$0002+1, draw)
}
}
sub plot(uword @zp xx, uword @zp yy, bool @zp draw) {
+17 -17
View File
@@ -2,7 +2,6 @@ package prog8
import kotlinx.cli.*
import prog8.ast.AstException
import prog8.code.core.Position
import prog8.code.source.ImportFileSystem
import prog8.code.source.ImportFileSystem.expandTilde
import prog8.code.target.CompilationTargets
@@ -221,6 +220,7 @@ private fun compileMain(args: Array<String>): Boolean {
breakpointCpuInstruction,
printAst1 == true,
printAst2 == true,
printCompileInfo = true,
ignoreFootguns == true,
profilingInstrumentation == true,
traceImports == true,
@@ -306,11 +306,12 @@ private fun compileMain(args: Array<String>): Boolean {
breakpointCpuInstruction,
printAst1 == true,
printAst2 == true,
true,
ignoreFootguns == true,
profilingInstrumentation == true,
traceImports == true,
processedSymbols,
absoluteSrcDirs,
traceImports == true,
processedSymbols,
absoluteSrcDirs,
outputPath,
cwd = Path.of(System.getProperty("user.dir")),
errors = ErrorReporter(txtcolors)
@@ -371,6 +372,7 @@ private fun compileMain(args: Array<String>): Boolean {
breakpointCpuInstruction,
printAst1 == true,
printAst2 == true,
true,
ignoreFootguns == true,
profilingInstrumentation == true,
traceImports == true,
@@ -705,6 +707,7 @@ private fun compileViaDaemon(compilerArgs: CompilerArguments, plainText: Boolean
println("Using existing prog8c daemon at $socketPath")
}
printCompileStart(compilerArgs)
val response = communicateWithDaemon(channel, compilerArgs, plainText)
if (response != null && (response.ok || !wasExisting || response.versionError == null)) return response
@@ -725,6 +728,15 @@ private fun compileViaDaemon(compilerArgs: CompilerArguments, plainText: Boolean
return communicateWithDaemon(channel, compilerArgs, plainText)
}
private fun printCompileStart(args: CompilerArguments) {
if (args.quietAll)
return
val programPath = args.filepath.toAbsolutePath().normalize()
val display = args.cwd.toAbsolutePath().relativize(programPath)
println("Compiling program $display")
println("Compiler target: ${getCompilationTargetByName(args.compilationTarget).name}")
}
private fun startDaemonProcess(): Boolean {
return try {
val javaHome = System.getProperty("java.home")
@@ -784,6 +796,7 @@ private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: Compiler
val request = DaemonRequest(
version = prog8.buildversion.BUILD_UNIX_TIME.toString(),
plainText = plainText,
filepath = compilerArgs.filepath.toString(),
optimize = compilerArgs.optimize,
writeAssembly = compilerArgs.writeAssembly,
@@ -836,22 +849,9 @@ private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: Compiler
return response
}
val txtcolors = if(plainText) ErrorReporter.PlainText else ErrorReporter.AnsiColors
val reporter = ErrorReporter(txtcolors)
for (error in response.errors) {
val pos = Position(error.file ?: "", error.line, error.startCol, error.endCol)
when (error.severity) {
"ERROR" -> reporter.err(error.message, pos)
"WARNING" -> reporter.warn(error.message, pos)
"INFO" -> reporter.info(error.message, pos)
}
}
reporter.report()
System.out.print(response.stdout)
System.out.flush()
System.err.print(response.stderr)
System.out.flush()
System.err.flush()
return response
+8 -3
View File
@@ -65,6 +65,7 @@ class CompilerArguments(val filepath: Path,
val breakpointCpuInstruction: String?,
val printAst1: Boolean,
val printAst2: Boolean,
val printCompileInfo: Boolean = true,
val ignoreFootguns: Boolean,
val profilingInstrumentation: Boolean,
val traceImports: Boolean = false,
@@ -112,8 +113,9 @@ fun compileProgram(args: CompilerArguments): CompilationResult? {
libraryDirs,
args.cwd,
args.quietAll,
args.printCompileInfo,
args.traceImports
)
)
}
val (program, options, imported) = parseresult
@@ -411,12 +413,13 @@ fun parseMainModule(filepath: Path,
libraryDirs: List<String>,
cwd: Path,
quiet: Boolean,
traceImports: Boolean): Triple<Program, CompilationOptions, List<Path>> {
printCompileInfo: Boolean = true,
traceImports: Boolean = false): Triple<Program, CompilationOptions, List<Path>> {
val bf = BuiltinFunctionsFacade(BuiltinFunctions)
val program = Program(filepath.nameWithoutExtension, bf, compTarget, compTarget)
bf.program = program
val importer = ModuleImporter(program, compTarget.name, errors, sourceDirs, libraryDirs, cwd, quiet, traceImports)
val importer = ModuleImporter(program, compTarget.name, errors, sourceDirs, libraryDirs, cwd, quiet, printCompileInfo, traceImports)
val importedModuleResult = importer.importMainModule(filepath)
importedModuleResult.onErr { throw it }
errors.report()
@@ -535,6 +538,8 @@ internal fun determineCompilationOptions(program: Program, compTarget: ICompilat
}
private fun processAst(program: Program, errors: IErrorReporter, compilerOptions: CompilationOptions) {
program.checkVarDeclsOnOwnLine(errors)
errors.report()
program.preprocessAst(errors, compilerOptions)
if(errors.noErrors() && compilerOptions.dumpSymbols) {
printSymbols(program)
+6 -37
View File
@@ -1,9 +1,8 @@
package prog8.compiler
import prog8.buildversion.BUILD_UNIX_TIME
import prog8.code.core.Position
import prog8.code.core.IErrorReporter
import prog8.code.source.ImportFileSystem
import prog8.code.source.SourceCode
import java.io.*
import java.net.StandardProtocolFamily
import java.net.UnixDomainSocketAddress
@@ -104,7 +103,6 @@ internal class CompilerDaemon(private val socketPath: Path) {
val resp = DaemonResponse(
ok = false,
versionError = "daemon version mismatch: build time differs",
errors = emptyList(),
stdout = "",
stderr = "",
t_ms = 0,
@@ -127,17 +125,16 @@ internal class CompilerDaemon(private val socketPath: Path) {
System.setOut(combinedStream)
System.setErr(combinedStream)
val daemonErr = DaemonErrorReporter()
val reporter = ErrorReporter(if (request.plainText) ErrorReporter.PlainText else ErrorReporter.AnsiColors)
var result: CompilationResult? = null
ImportFileSystem.clearCaches()
val t_ms = try {
measureTime {
val compilerArgs = request.toCompilerArguments(daemonErr)
val compilerArgs = request.toCompilerArguments(reporter)
result = compileProgram(compilerArgs)
}.toLong(DurationUnit.MILLISECONDS)
} catch (e: Exception) {
System.err.println("daemon: compilation crash: ${e.message}")
daemonErr.err("internal daemon error: ${e.message}", Position.DUMMY)
0L
} finally {
System.out.flush()
@@ -169,35 +166,6 @@ internal class CompilerDaemon(private val socketPath: Path) {
val resp = DaemonResponse(
ok = result != null,
versionError = null,
errors = daemonErr.getMessages().map { msg ->
// Strip "ERROR ", "WARNING ", "INFO " prefixes
var message = if (msg.message.startsWith("${msg.severity} ")) {
msg.message.substring(msg.severity.length + 1)
} else {
msg.message
}
// Strip path prefix if it exists (e.g. file:///...:line:col: )
// The path prefix usually ends with a colon followed by a space
val pathRegex = Regex("^file:///[^:]+:[0-9]+:[0-9]+: ")
message = message.replace(pathRegex, "")
DaemonError(
severity = msg.severity,
message = message,
file = msg.position?.let {
val origin = it.file
if (SourceCode.isLibraryResource(origin)) {
origin
} else {
ImportFileSystem.userHome.resolve(origin).normalize().toString()
}
},
line = msg.position?.line ?: 1,
startCol = msg.position?.startCol ?: 1,
endCol = msg.position?.endCol ?: 1
)
},
stdout = stdout,
stderr = stderr,
t_ms = t_ms,
@@ -214,7 +182,7 @@ internal class CompilerDaemon(private val socketPath: Path) {
}
}
private fun DaemonRequest.toCompilerArguments(daemonErr: DaemonErrorReporter): CompilerArguments {
private fun DaemonRequest.toCompilerArguments(errors: IErrorReporter): CompilerArguments {
// Resolve the output directory path: if relative, make it absolute by resolving against
// the client's working directory (cwd) to avoid writing files in the daemon's CWD.
val clientCwd = Path.of(cwd)
@@ -244,6 +212,7 @@ internal class CompilerDaemon(private val socketPath: Path) {
breakpointCpuInstruction = breakpointCpuInstruction,
printAst1 = printAst1,
printAst2 = printAst2,
printCompileInfo = false,
ignoreFootguns = ignoreFootguns,
profilingInstrumentation = profilingInstrumentation,
traceImports = traceImports,
@@ -251,7 +220,7 @@ internal class CompilerDaemon(private val socketPath: Path) {
sourceDirs = sourceDirs,
outputDir = resolvedOutputDir,
cwd = clientCwd,
errors = daemonErr
errors = errors
)
}
}
@@ -1,48 +0,0 @@
package prog8.compiler
import prog8.code.core.ErrorsReportedException
import prog8.code.core.IErrorReporter
import prog8.code.core.Position
internal class DaemonErrorReporter : IErrorReporter {
data class CompilerMessage(val severity: String, val message: String, val position: Position?)
private val messages = mutableListOf<CompilerMessage>()
override fun err(msg: String, position: Position) {
messages.add(CompilerMessage("ERROR", msg, position))
}
override fun warn(msg: String, position: Position) {
messages.add(CompilerMessage("WARNING", msg, position))
}
override fun info(msg: String, position: Position) {
messages.add(CompilerMessage("INFO", msg, position))
}
override fun undefined(symbol: List<String>, suggestImport: Boolean, position: Position) {
if (suggestImport && symbol.size > 1)
err("undefined symbol: ${symbol.joinToString(".")} (maybe you forgot to import a module that defines ${symbol.first()}?)", position)
else
err("undefined symbol: ${symbol.joinToString(".")}", position)
}
override fun noErrors() = messages.none { it.severity == "ERROR" }
override fun report() {
// Don't print anything, but throw so compileProgram() stops and returns null
if (!noErrors())
throw ErrorsReportedException("")
}
override fun noErrorForLine(position: Position) =
!messages.any { it.position?.line == position.line && it.severity != "INFO" }
override fun printSingleError(errormessage: String) {
messages.add(CompilerMessage("ERROR", errormessage, null))
}
fun getMessages() = messages.toList()
}
+8 -41
View File
@@ -3,6 +3,7 @@ package prog8.compiler
internal data class DaemonRequest(
val version: String,
val plainText: Boolean,
val filepath: String,
val optimize: Boolean,
val writeAssembly: Boolean,
@@ -35,7 +36,6 @@ internal data class DaemonRequest(
internal data class DaemonResponse(
val ok: Boolean,
val versionError: String?,
val errors: List<DaemonError>,
val stdout: String,
val stderr: String,
val t_ms: Long,
@@ -43,15 +43,6 @@ internal data class DaemonResponse(
val importedFiles: List<String>
)
internal data class DaemonError(
val severity: String,
val message: String,
val file: String?,
val line: Int,
val startCol: Int,
val endCol: Int
)
// Minimal JSON encoder/decoder for the daemon protocol
internal object DaemonProtocol {
@@ -59,6 +50,7 @@ internal object DaemonProtocol {
fun encodeRequest(req: DaemonRequest): String = buildString {
append('{')
append(prop("version", req.version))
append(prop("plainText", req.plainText))
append(prop("filepath", req.filepath))
append(prop("optimize", req.optimize))
append(prop("writeAssembly", req.writeAssembly))
@@ -95,21 +87,6 @@ internal object DaemonProtocol {
append('{')
append(prop("ok", resp.ok))
append(propOpt("versionError", resp.versionError))
append("\"errors\":[")
for ((i, e) in resp.errors.withIndex()) {
if (i > 0) append(',')
append('{')
append(prop("severity", e.severity))
append(prop("message", e.message))
append(propOpt("file", e.file))
append(prop("line", e.line))
append(prop("startCol", e.startCol))
append(prop("endCol", e.endCol))
append("null")
setLength(length - 4)
append('}')
}
append("],")
append(prop("stdout", resp.stdout))
append(prop("stderr", resp.stderr))
append(prop("t_ms", resp.t_ms))
@@ -122,24 +99,11 @@ internal object DaemonProtocol {
fun decodeResponse(json: String): DaemonResponse {
val map = parseJsonObject(json)
val errorsJson = map["errors"] as? List<*> ?: emptyList<Any>()
val errors = errorsJson.map { e ->
val em = e as Map<*, *>
DaemonError(
severity = em["severity"] as String,
message = em["message"] as String,
file = em["file"] as? String,
line = (em["line"] as Number).toInt(),
startCol = (em["startCol"] as Number).toInt(),
endCol = (em["endCol"] as Number).toInt()
)
}
val outputFiles = (map["outputFiles"] as? List<*>)?.map { it as String } ?: emptyList()
val outputFiles = (map["outputFiles"] as? List<*>)?.map { it as String } ?: emptyList<String>()
val importedFiles = (map["importedFiles"] as? List<*>)?.map { it as String } ?: emptyList()
return DaemonResponse(
ok = map["ok"] as Boolean,
versionError = map["versionError"] as? String,
errors = errors,
stdout = map["stdout"] as String,
stderr = map["stderr"] as String,
t_ms = (map["t_ms"] as Number).toLong(),
@@ -152,6 +116,7 @@ internal object DaemonProtocol {
val map = parseJsonObject(json)
return DaemonRequest(
version = map["version"] as String,
plainText = map["plainText"] as Boolean,
filepath = map["filepath"] as String,
optimize = map["optimize"] as Boolean,
writeAssembly = map["writeAssembly"] as Boolean,
@@ -266,10 +231,12 @@ internal object DaemonProtocol {
private fun parseStringValue(s: String, start: Int): Pair<String, Int> {
check(s[start] == '"')
pos@ for (pos in (start + 1) until s.length) {
var pos = start + 1
while (pos < s.length) {
when (s[pos]) {
'\\' -> continue@pos // skip next char
'\\' -> pos += 2
'"' -> return unescapeJson(s.substring(start + 1, pos)) to (pos + 1)
else -> pos++
}
}
error("unterminated string in JSON")
@@ -26,6 +26,7 @@ class ModuleImporter(private val program: Program,
libraryDirs: List<String>,
val cwd: Path,
val quiet: Boolean,
val printCompileInfo: Boolean = true,
val traceImports: Boolean = false) {
private val sourcePaths: List<Path> = sourceDirs.map { Path(it).sanitize() }.distinct()
@@ -61,7 +62,7 @@ class ModuleImporter(private val program: Program,
}
private fun printCompileInfo(programPath: Path) {
if(!quiet) {
if(!quiet && printCompileInfo) {
println("Compiling program ${cwd.toAbsolutePath().relativize(programPath)}")
println("Compiler target: $compilationTargetName")
}
@@ -175,17 +176,20 @@ class ModuleImporter(private val program: Program,
return result.mapError { NoSuchFileException(File(name)) }
}
private fun getModuleFromFile(name: String, importingModule: Module?): Pair<Result<SourceCode, NoSuchFileException>, List<Path>> {
internal fun getModuleFromFile(name: String, importingModule: Module?): Pair<Result<SourceCode, NoSuchFileException>, List<Path>> {
val fileName = "$name.p8"
val requestedBy = importingModule?.name ?: "~implicit~"
val cwd = Path("").absolute()
val normalLocations =
if (importingModule == null) {
(sourcePaths + listOf(Path("").absolute())).distinct()
} else {
(sourcePaths + listOf(cwd)).distinct()
} else if (SourceCode.isRegularFilesystemPath(importingModule.position.file)) {
val pathFromImportingModule = (Path(importingModule.position.file).parent ?: Path("")).sanitize()
val cwd = Path("").absolute()
(sourcePaths + listOf(pathFromImportingModule, cwd)).distinct()
} else {
// library/string resources have no neighboring directory on disk
(sourcePaths + listOf(cwd)).distinct()
}
val libraryPathsSet = libraryPaths.toSet()
@@ -2071,7 +2071,7 @@ internal class AstChecker(private val program: Program,
private fun checkPointer(pointervar: IdentifierReference) {
val vardecl = pointervar.targetVarDecl()
if(vardecl?.zeropage == ZeropageWish.NOT_IN_ZEROPAGE)
errors.info("pointer variable should preferrably be in zeropage but is marked nozp", vardecl.position)
errors.info("pointer variable should preferably be in zeropage but is marked nozp", vardecl.position)
}
override fun visit(arrayIndexedExpression: ArrayIndexedExpression) {
@@ -2136,6 +2136,23 @@ internal class AstChecker(private val program: Program,
if(!dt.isPointer && !dt.isUnsignedWord && !dt.isIterable) {
errors.err("cannot array index on this field type", arrayIndexedExpression.indexer.position)
}
// check index out of bounds for struct field arrays via pointer
val constIdx = arrayIndexedExpression.indexer.constIndex()
if(constIdx!=null) {
val deref = arrayIndexedExpression.pointerderef!!
val target = deref.definingScope.lookup(deref.chain)
val arraysize: Int? = when(target) {
is StructFieldRef -> target.field.constSize()
is VarDecl -> target.arraysize?.constIndex()
else -> null
}
if(arraysize!=null) {
if(constIdx !in 0..<arraysize)
errors.err("index out of bounds", arrayIndexedExpression.indexer.position)
} else if(constIdx < 0) {
errors.err("index out of bounds", arrayIndexedExpression.indexer.position)
}
}
// else if(target is StructFieldRef) {
// if(!target.type.isPointer && !target.type.isUnsignedWord)
// errors.err("cannot array index on this field type", arrayIndexedExpression.indexer.position)
@@ -2314,14 +2331,28 @@ internal class AstChecker(private val program: Program,
if(uniqueFields.isEmpty())
errors.err("struct must contain at least one field", struct.position)
fun validFieldType(field: StructField): Boolean {
val dt=field.type
if(dt.isArray && dt.subType!=null) {
errors.err("arrays of typed pointers are currently not supported, use untyped pointer for now at field '${field.name}'", struct.position)
return false
}
return dt.isBasic
|| dt.base.isNumericOrBool
|| dt.isPointer
|| dt.isArray
}
struct.fields.forEach { field ->
val dt = field.type
val allowed = dt.isBasic
|| dt.base.isNumericOrBool
|| dt.isPointer
|| (dt.isArray && dt.sub!!.isNumericOrBool)
if(!allowed)
errors.err("only booleans, numeric and pointer fields allowed in a struct: '${field.name}'", struct.position)
if(!validFieldType(field))
errors.err("invalid struct field type at '${field.name}'", struct.position)
if(field.isArray) {
val size = field.constSize()
if(size==null)
errors.err("array length must be known at compile-time", field.arraySize?.position ?: struct.position)
else if(size<=0)
errors.err("array length must be positive", field.arraySize?.position ?: struct.position)
}
}
}
@@ -2723,7 +2754,7 @@ internal class AstChecker(private val program: Program,
val struct = initializer.structname.targetStructDecl()
if(struct!=null) {
val expectedFieldCount = struct.fields.size
val expectedFlattenedElements = struct.fields.sumOf { it.arraySize ?: 1 }
val expectedFlattenedElements = struct.fields.sumOf { it.constSize() ?: 1 }
val flattenedArgs = args.flattenArgs()
if (args.size != expectedFieldCount) {
@@ -5,10 +5,7 @@ import prog8.ast.Node
import prog8.ast.Program
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.ast.walk.AstModification
import prog8.ast.walk.AstRemove
import prog8.ast.walk.AstReplaceNode
import prog8.ast.walk.AstWalker
import prog8.ast.walk.*
import prog8.code.core.*
import java.io.CharConversionException
@@ -24,8 +21,47 @@ internal fun invalidNumberOfArgsError(errors: IErrorReporter, pos: Position, num
errors.err("invalid number of arguments: expected $expected but got $numArgs", pos)
}
internal fun Program.checkValid(errors: IErrorReporter, compilerOptions: CompilationOptions) {
// semantic analysis to see if the program is valid.
internal fun Program.checkVarDeclsOnOwnLine(errors: IErrorReporter) {
// A variable declaration directly followed by another statement on the same
// source line (e.g. `ubyte length foo(42)`) parses as two statements, but it is
// almost certainly a missing '=' or newline and very confusing, so report it.
// This must run on the freshly parsed tree: later passes split off initializer
// assignments and remove unused variables, which destroys the original adjacency.
fun checkStatements(statements: List<Statement>) {
for (i in 0 until statements.size - 1) {
val cur = statements[i] as? VarDecl ?: continue
if (cur.origin != VarDeclOrigin.USERCODE) continue
val next = statements[i + 1]
val curPos = cur.position
val nextPos = next.position
if (curPos.file == nextPos.file && curPos.line != 0 && curPos.line == nextPos.line && nextPos.startCol > curPos.endCol) {
errors.err("variable declaration must be on its own line (did you forget '=' or a newline?)", nextPos)
}
}
}
val checker = object : IAstVisitor {
override fun visit(block: Block) {
checkStatements(block.statements)
super.visit(block)
}
override fun visit(subroutine: Subroutine) {
checkStatements(subroutine.statements)
super.visit(subroutine)
}
override fun visit(scope: AnonymousScope) {
checkStatements(scope.statements)
super.visit(scope)
}
override fun visit(decl: VarDecl) {
// Do not traverse into the declaration: its value can still be an
// ExpressionTuple at this stage, which cannot be visited.
}
}
checker.visit(this)
}
internal fun Program.checkValid(errors: IErrorReporter, compilerOptions: CompilationOptions) { // semantic analysis to see if the program is valid.
val parentChecker = ParentNodeChecker()
parentChecker.visit(this)
val checker = AstChecker(this, errors, compilerOptions)
@@ -11,6 +11,7 @@ import prog8.ast.statements.*
import prog8.ast.walk.IAstVisitor
import prog8.code.core.*
import prog8.code.target.VMTarget
import prog8.parser.Prog8Keywords
/**
* This checks for naming conflicts, not for correct symbol references yet.
@@ -30,15 +31,21 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
}
override fun visit(block: Block) {
val existing = blocks[block.name]
if(existing!=null) {
if (block.isInLibrary)
nameError(existing.name, existing.position, block)
if(block.name in BuiltinFunctions)
errors.err("builtin function cannot be redefined", block.position)
else if(block.name in keywords)
errors.err("block name cannot be a keyword", block.position)
else {
val existing = blocks[block.name]
if(existing!=null) {
if (block.isInLibrary)
nameError(existing.name, existing.position, block)
else
nameError(block.name, block.position, existing)
}
else
nameError(block.name, block.position, existing)
blocks[block.name] = block
}
else
blocks[block.name] = block
super.visit(block)
}
@@ -79,41 +86,8 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
super.visit(decl)
}
private val keywords = setOf(
"void",
"on",
"in",
"to",
"if",
"downto",
"else",
"step",
"call",
"goto",
"inline",
"struct",
"then",
"alias",
"defer",
"const",
"sizeof",
"as",
"return",
"break",
"continue",
"true",
"false",
"sub",
"asmsub",
"extsub",
"clobbers",
"while",
"do",
"until",
"repeat",
"unroll",
"when"
)
// the set of reserved keywords in the prog8 language
private val keywords: Set<String> by lazy { Prog8Keywords.reservedWords }
override fun visit(struct: StructDecl) {
if(struct.name in BuiltinFunctions)
@@ -121,7 +95,6 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
else if(struct.name in keywords)
errors.err("struct name cannot be a keyword", struct.position)
super.visit(struct)
}
@@ -151,17 +151,47 @@ class AstPreprocessor(val program: Program,
if(decl.names.size>1) {
// we need to handle multi-decl here too, the desugarer maybe has not processed it here yet...
if(decl.value!=null) {
decl.names.forEach { name ->
val target = AssignTarget(
IdentifierReference(listOf(name), decl.position),
null,
null,
null,
false,
position = decl.position
// only use a multi-value assignment if the initializer is a function call with multiple
// return values, so that the call is only performed once. Otherwise (e.g. a constant
// initializer) we have to fall back to individual assignments.
val fcall = decl.value as? IFunctionCall
val fcallTarget = fcall?.target?.targetSubroutine()
val isBuiltinMultiReturn = fcall?.target?.let { target ->
val name = target.nameInSource.singleOrNull()
name != null && name in program.builtinFunctions.names &&
program.builtinFunctions.returnTypes(name).size > 1
} ?: false
if(fcallTarget!=null || isBuiltinMultiReturn) {
val variables = decl.names.map { name ->
AssignTarget(
IdentifierReference(listOf(name), decl.position),
null,
null,
null,
false,
position = decl.position
)
}
val assign = Assignment(
AssignTarget(null, null, null, variables, false, position = decl.position),
decl.value!!,
AssignmentOrigin.VARINIT,
decl.position
)
val assign = Assignment(target.copy(), decl.value!!.copy(), AssignmentOrigin.VARINIT, decl.position)
replacements.add(AstInsert.after(decl, assign, scope))
} else {
decl.names.forEach { name ->
val target = AssignTarget(
IdentifierReference(listOf(name), decl.position),
null,
null,
null,
false,
position = decl.position
)
val assign = Assignment(target, decl.value!!.copy(), AssignmentOrigin.VARINIT, decl.position)
replacements.add(AstInsert.after(decl, assign, scope))
}
}
replacements.add(AstRemove(decl, scope))
decl.value = null
@@ -390,10 +420,16 @@ class AstPreprocessor(val program: Program,
val tgt = alias.target.targetStatement(program.builtinFunctions)
if(tgt==null) {
// The target may refer to an enum member that hasn't been desugared into consts yet.
// Defer the error in that case; a later iteration will resolve it after desugaring.
if(refersToNotYetDesugaredEnumMember(alias))
return noModifications
errors.undefined(alias.target.nameInSource, position = alias.target.position)
} else {
if(alias.alias == alias.target.nameInSource.first()) {
errors.err("alias loop", alias.position)
} else if(tgt is Enumeration) {
errors.err("cannot alias an enum '${alias.target.nameInSource.joinToString(".")}', alias individual members instead (e.g. ${alias.target.nameInSource.joinToString(".")}::Member)", alias.position)
} else if(tgt is Alias) {
var chainedAlias = alias
var chainedTargetName = alias.target
@@ -433,6 +469,23 @@ class AstPreprocessor(val program: Program,
return noModifications
}
private fun refersToNotYetDesugaredEnumMember(alias: Alias): Boolean {
val nameInSource = alias.target.nameInSource
if(nameInSource.isEmpty())
return false
val last = nameInSource.last()
if("::" !in last)
return false
val parts = last.split("::", limit = 2)
if(parts.size != 2 || parts[0].isEmpty() || parts[1].isEmpty())
return false
val enumName = parts[0]
val memberName = parts[1]
val enumPath = nameInSource.dropLast(1) + enumName
val enumNode = alias.definingScope.lookup(enumPath) as? Enumeration ?: return false
return enumNode.members.any { it.first == memberName }
}
override fun after(typecast: TypecastExpression, parent: Node): Iterable<AstModification> {
// convert all antlr names to structs
if(typecast.type.subTypeFromAntlr!=null) {
@@ -589,7 +589,13 @@ _after:
if(expr.operator==".") {
if(expr.left is IdentifierReference) {
require(expr.right !is IdentifierReference)
if(expr.right is IdentifierReference) {
// (a.b).c -> a.b.c - merge dotted names (handles parenthesized pointer chains like (t.next).flag)
val leftIdent = expr.left as IdentifierReference
val rightIdent = expr.right as IdentifierReference
val combined = IdentifierReference(leftIdent.nameInSource + rightIdent.nameInSource, expr.position)
return listOf(AstReplaceNode(expr, combined, parent))
}
val ri = expr.right as? ArrayIndexedExpression
if(ri!=null && ri.plainarrayvar!=null) {
val leftIdent = expr.left as IdentifierReference
@@ -648,6 +654,52 @@ _after:
val ident = IdentifierReference(right.chain, right.position)
return listOf(AstReplaceNode(expr.right, ident, expr))
}
// Generic pointer field access on complex expressions, e.g. (expr as ^^Struct).field
// Simple identifier/array left-hand-sides (a.b.c, ptr[i].field) are handled elsewhere.
// Writes through such an expression can't occur: the grammar only allows them as read expressions.
if(expr.left !is IdentifierReference && expr.left !is ArrayIndexedExpression) {
val fieldIdent = expr.right as? IdentifierReference
if(fieldIdent!=null) {
val leftDt = expr.left.inferType(program).getOrUndef()
val struct = if(leftDt.isPointer) leftDt.subType as? StructDecl else null
val fieldName = fieldIdent.nameInSource.first()
val fieldDt = struct?.getFieldType(fieldName)
if(struct!=null && fieldDt!=null) {
// (ptr).field --> peekXXX(ptr as uword + offsetof(Struct.field))
val offset = struct.offsetof(fieldName, program.memsizer)!!.toInt()
val ptrAsUword = TypecastExpression(expr.left.copy(), DataType.UWORD, true, expr.left.position)
val address: Expression = if(offset==0) ptrAsUword
else BinaryExpression(ptrAsUword, "+", NumericLiteral.optimalInteger(offset, expr.position), expr.position)
fun peekCall(func: String) =
FunctionCallExpression(IdentifierReference(listOf(func), expr.position), mutableListOf(address), expr.position)
val readExpr: Expression = when {
fieldDt.isBool -> peekCall("peekbool")
fieldDt.isUnsignedByte -> DirectMemoryRead(address, expr.position)
fieldDt.isSignedByte -> TypecastExpression(DirectMemoryRead(address, expr.position), DataType.BYTE, true, expr.position)
fieldDt.isUnsignedWord -> peekCall("peekw")
fieldDt.isSignedWord -> TypecastExpression(peekCall("peekw"), DataType.WORD, true, expr.position)
fieldDt.isLong -> peekCall("peekl")
fieldDt.isFloat -> peekCall("peekf")
fieldDt.isPointer -> TypecastExpression(peekCall("peekw"), fieldDt, true, expr.position)
else -> {
errors.err("unsupported field type for pointer dereference", expr.position)
return noModifications
}
}
if(fieldIdent.nameInSource.size==1)
return listOf(AstReplaceNode(expr, readExpr, parent))
// (ptr).field.rest... --> (read of first field) . rest... (will be desugared further in the next pass)
if(!fieldDt.isPointer) {
errors.err("cannot dereference non-pointer field '$fieldName'", fieldIdent.position)
return noModifications
}
val rest = IdentifierReference(fieldIdent.nameInSource.drop(1), fieldIdent.position)
val chained = BinaryExpression(readExpr, ".", rest, expr.position)
return listOf(AstReplaceNode(expr, chained, parent))
}
}
}
}
return noModifications
@@ -149,7 +149,7 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
}
}
if(target==null && identifier.nameInSource.size>1) {
if(identifier.nameInSource.size>1) {
// First component might be an alias
val tgt2 = identifier.definingScope.lookup(identifier.nameInSource[0]) as? Alias
if(tgt2!=null && parent !is Alias) {
@@ -528,9 +528,9 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
private fun transform(initializer: StaticStructInitializer): PtFunctionCall {
val targetStruct = initializer.structname.targetStructDecl()!!
val call = PtFunctionCall("prog8_lib_structalloc", true, true, arrayOf(DataType.pointer(targetStruct)), initializer.position)
val flattenedArgs = initializer.args.flattenArgs()
val expectedTotalElements = targetStruct.fields.sumOf { it.arraySize ?: 1 }
val expectedTotalElements = targetStruct.fields.sumOf { it.constSize() ?: 1 }
if (flattenedArgs.isNotEmpty() && flattenedArgs.size != expectedTotalElements) {
errors.err("invalid number of field values: expected $expectedTotalElements flattened elements for struct '${targetStruct.name}' but got ${flattenedArgs.size}", initializer.position)
}
@@ -817,7 +817,12 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
}
private fun transform(struct: StructDecl): PtStructDecl {
return PtStructDecl(struct.name, struct.fields.map { PtStructField(it.type, it.name, it.arraySize) }, struct.position)
return PtStructDecl(struct.name, struct.fields.map {
val size = it.constSize()
if(it.isArray && size==null)
errors.err("array length must be known at compile-time", it.arraySize?.position ?: struct.position)
PtStructField(it.type, it.name, size)
}, struct.position)
}
private fun transform(srcWhen: When): PtWhen {
+17 -17
View File
@@ -152,7 +152,7 @@ class TestCompilerOnExamplesC64: FunSpec({
ExampleSizes("bdmusic-irq", c64SizeOptimized=1001, c64SizeUnoptimized=1019),
ExampleSizes("bdmusic", c64SizeOptimized=938, c64SizeUnoptimized=1011),
ExampleSizes("charset", c64SizeOptimized=835, c64SizeUnoptimized=860),
ExampleSizes("cube3d-sprites", c64SizeOptimized=2664, c64SizeUnoptimized=2769),
ExampleSizes("cube3d-sprites", c64SizeOptimized=2664, c64SizeUnoptimized=2705),
ExampleSizes("fileselector", c64SizeOptimized=3523, c64SizeUnoptimized=4510),
ExampleSizes("library/main", c64SizeOptimized=321, c64SizeUnoptimized=1147),
ExampleSizes("multiplexer", c64SizeOptimized=1356, c64SizeUnoptimized=1507),
@@ -162,7 +162,7 @@ class TestCompilerOnExamplesC64: FunSpec({
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=900),
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1028),
ExampleSizes("starfield", c64SizeOptimized=743, c64SizeUnoptimized=758),
ExampleSizes("turtle-gfx", c64SizeOptimized=2701, c64SizeUnoptimized=2806),
ExampleSizes("turtle-gfx", c64SizeOptimized=2701, c64SizeUnoptimized=2812),
ExampleSizes("wizzine", c64SizeOptimized=1124, c64SizeUnoptimized=1224),
// boingball, cube3d, tehtriz are in BothC64andCx16
),
@@ -211,7 +211,7 @@ class TestCompilerOnExamplesCx16: FunSpec({
ExampleSizes("fileselector/main", cx16SizeOptimized=480, cx16SizeUnoptimized=2314),
// fileselector/fselector and fileselector/namesorting don't compile (empty source files)
ExampleSizes("pointers/fountain-cx16", cx16SizeOptimized=1179, cx16SizeUnoptimized=1520),
ExampleSizes("amiga", cx16SizeOptimized=7631, cx16SizeUnoptimized=11628),
ExampleSizes("amiga", cx16SizeOptimized=7810, cx16SizeUnoptimized=11827),
ExampleSizes("audioroutines", cx16SizeOptimized=948, cx16SizeUnoptimized=1244),
ExampleSizes("automatons", cx16SizeOptimized=1037, cx16SizeUnoptimized=1342),
ExampleSizes("balloonflight", cx16SizeOptimized=3187, cx16SizeUnoptimized=4202),
@@ -220,8 +220,8 @@ class TestCompilerOnExamplesCx16: FunSpec({
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1367, cx16SizeUnoptimized=1883),
ExampleSizes("charfade", cx16SizeOptimized=1054, cx16SizeUnoptimized=1376),
ExampleSizes("charsets", cx16SizeOptimized=1125, cx16SizeUnoptimized=1417),
ExampleSizes("circles", cx16SizeOptimized=2377, cx16SizeUnoptimized=4495),
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7756, cx16SizeUnoptimized=10434),
ExampleSizes("circles", cx16SizeOptimized=2535, cx16SizeUnoptimized=4689),
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7642, cx16SizeUnoptimized=10368),
ExampleSizes("colorbars", cx16SizeOptimized=1606, cx16SizeUnoptimized=1904),
ExampleSizes("cxlogo", cx16SizeOptimized=599, cx16SizeUnoptimized=895),
ExampleSizes("diskspeed", cx16SizeOptimized=6117, cx16SizeUnoptimized=8091),
@@ -243,9 +243,9 @@ class TestCompilerOnExamplesCx16: FunSpec({
ExampleSizes("sortingbench", cx16SizeOptimized=2928, cx16SizeUnoptimized=3857),
ExampleSizes("spotlight", cx16SizeOptimized=908, cx16SizeUnoptimized=1364),
ExampleSizes("starszoom", cx16SizeOptimized=2399, cx16SizeUnoptimized=4820),
ExampleSizes("test_gfx_hires", cx16SizeOptimized=4760, cx16SizeUnoptimized=7444),
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5820, cx16SizeUnoptimized=7004),
ExampleSizes("testmonogfx", cx16SizeOptimized=10255, cx16SizeUnoptimized=11631),
ExampleSizes("test_gfx_hires", cx16SizeOptimized=4966, cx16SizeUnoptimized=7650),
ExampleSizes("test_gfx_lores", cx16SizeOptimized=6019, cx16SizeUnoptimized=7182),
ExampleSizes("testmonogfx", cx16SizeOptimized=10468, cx16SizeUnoptimized=11847),
ExampleSizes("textspotlight", cx16SizeOptimized=3431, cx16SizeUnoptimized=3788),
),
listOf(false, true)
@@ -276,9 +276,9 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
listOf(
ExampleSizes("balls", c64SizeOptimized=885, c64SizeUnoptimized=927, cx16SizeOptimized=874, cx16SizeUnoptimized=1200),
ExampleSizes("boingball", c64SizeOptimized=1487, c64SizeUnoptimized=1553, cx16SizeOptimized=1463, cx16SizeUnoptimized=1944),
ExampleSizes("cube3d", c64SizeOptimized=2436, c64SizeUnoptimized=2513, cx16SizeOptimized=2373, cx16SizeUnoptimized=2704),
ExampleSizes("cube3d-float", c64SizeOptimized=2591, c64SizeUnoptimized=2643, cx16SizeOptimized=2447, cx16SizeUnoptimized=2770),
ExampleSizes("cube3d-gfx", c64SizeOptimized=3528, c64SizeUnoptimized=3625, cx16SizeOptimized=2198, cx16SizeUnoptimized=2522),
ExampleSizes("cube3d", c64SizeOptimized=2360, c64SizeUnoptimized=2446, cx16SizeOptimized=2299, cx16SizeUnoptimized=2645),
ExampleSizes("cube3d-float", c64SizeOptimized=2390, c64SizeUnoptimized=2441, cx16SizeOptimized=2283, cx16SizeUnoptimized=2606),
ExampleSizes("cube3d-gfx", c64SizeOptimized=3468, c64SizeUnoptimized=3600, cx16SizeOptimized=2138, cx16SizeUnoptimized=2482),
ExampleSizes("dirlist", c64SizeOptimized=1515, c64SizeUnoptimized=2291, cx16SizeOptimized=1525, cx16SizeUnoptimized=3270),
ExampleSizes("fibonacci", c64SizeOptimized=582, c64SizeUnoptimized=601, cx16SizeOptimized=634, cx16SizeUnoptimized=933),
ExampleSizes("fractal-tree", c64SizeOptimized=2778, c64SizeUnoptimized=2821, cx16SizeOptimized=1168, cx16SizeUnoptimized=1462),
@@ -331,16 +331,16 @@ class TestCompilerOnExamplesVirtual: FunSpec({
val onlyVirtual = cartesianProduct(
listOf(
ExampleSizes("bouncegfx", virtualInstrCountOptimized=240, virtualInstrCountUnoptimized=856, virtualRegCountOptimized=97, virtualRegCountUnoptimized=386),
ExampleSizes("bsieve", virtualInstrCountOptimized=273, virtualInstrCountUnoptimized=2001, virtualRegCountOptimized=74, virtualRegCountUnoptimized=866),
ExampleSizes("bsieve", virtualInstrCountOptimized=273, virtualInstrCountUnoptimized=2011, virtualRegCountOptimized=74, virtualRegCountUnoptimized=866),
ExampleSizes("fountain", virtualInstrCountOptimized=207, virtualInstrCountUnoptimized=824, virtualRegCountOptimized=80, virtualRegCountUnoptimized=369),
ExampleSizes("pixelshader", virtualInstrCountOptimized=93, virtualInstrCountUnoptimized=308, virtualRegCountOptimized=17, virtualRegCountUnoptimized=115),
ExampleSizes("sincos", virtualInstrCountOptimized=335, virtualInstrCountUnoptimized=930, virtualRegCountOptimized=138, virtualRegCountUnoptimized=409),
ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=330, virtualInstrCountUnoptimized=2145, virtualRegCountOptimized=111, virtualRegCountUnoptimized=939),
ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=658, virtualInstrCountUnoptimized=2327, virtualRegCountOptimized=260, virtualRegCountUnoptimized=1026),
ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=700, virtualInstrCountUnoptimized=2326, virtualRegCountOptimized=300, virtualRegCountUnoptimized=1051),
ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=404, virtualInstrCountUnoptimized=2488, virtualRegCountOptimized=130, virtualRegCountUnoptimized=1092),
ExampleSizes("pointers/animalgame", virtualInstrCountOptimized=330, virtualInstrCountUnoptimized=2155, virtualRegCountOptimized=111, virtualRegCountUnoptimized=939),
ExampleSizes("pointers/binarytree", virtualInstrCountOptimized=658, virtualInstrCountUnoptimized=2333, virtualRegCountOptimized=260, virtualRegCountUnoptimized=1026),
ExampleSizes("pointers/hashtable", virtualInstrCountOptimized=700, virtualInstrCountUnoptimized=2336, virtualRegCountOptimized=300, virtualRegCountUnoptimized=1051),
ExampleSizes("pointers/sortedlist", virtualInstrCountOptimized=404, virtualInstrCountUnoptimized=2494, virtualRegCountOptimized=130, virtualRegCountUnoptimized=1092),
ExampleSizes("pointers/fountain-virtual", virtualInstrCountOptimized=203, virtualInstrCountUnoptimized=819, virtualRegCountOptimized=75, virtualRegCountUnoptimized=363),
ExampleSizes("pointers/sorting", virtualInstrCountOptimized=530, virtualInstrCountUnoptimized=2322, virtualRegCountOptimized=183, virtualRegCountUnoptimized=1016)
ExampleSizes("pointers/sorting", virtualInstrCountOptimized=530, virtualInstrCountUnoptimized=2330, virtualRegCountOptimized=183, virtualRegCountUnoptimized=1016)
),
listOf(false, true)
)
+19
View File
@@ -8,13 +8,16 @@ import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldBeIn
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldStartWith
import prog8.ast.Program
import prog8.code.INTERNED_STRINGS_MODULENAME
import prog8.code.PROG8_CONTAINER_MODULES
import prog8.code.core.IErrorReporter
import prog8.code.source.ImportFileSystem
import prog8.code.source.SourceCode
import prog8.compiler.ModuleImporter
import prog8.parser.MultipleParseErrors
import prog8.parser.Prog8Parser
import prog8tests.helpers.*
import kotlin.io.path.*
@@ -190,6 +193,22 @@ class TestModuleImporter: FunSpec({
}
}
context("ImportFromResourceModule") {
test("neighbor directory search is skipped for modules loaded from internal library resources") {
val importer = makeImporter(null)
val resourceModule = Prog8Parser.parseModule(ImportFileSystem.getResource("/prog8lib/cx16/textio.p8"))
resourceModule.position.file shouldStartWith "library:"
val (result, searched) = importer.getModuleFromFile("syslib", resourceModule)
withClue("syslib only exists as an internal library resource, not on the filesystem") {
result.getErrorOrElse { error("expected a filesystem search failure") }.file.name shouldBe "syslib"
}
withClue("no filesystem search location may be derived from the library: origin: $searched") {
searched.none { it.toString().contains("library:") } shouldBe true
}
}
}
context("ImportLibraryModule") {
context("WithInvalidName") {
test("testWithNonExistingName") {
+101
View File
@@ -880,6 +880,107 @@ main {
ifStmt.condition shouldBe instanceOf<BinaryExpression>()
}
// The four cases below each cover one of the four "split into assign + compare" rewrite
// sites in StatementOptimizer (min/max x either operand order). All of them assign to the
// target before the comparison, so the compared operand must be a real constant.
fun assertNotReducedToOneOperand(source: String, expectedFuncPrefix: String) {
val result = compileText(Cx16Target(), optimize = true, source, outputDir, writeAssembly = false)!!
val stmts = result.compilerAst.entrypoint.statements
// the assignment under test is the last one before the implicit return
val assignment = stmts.filterIsInstance<Assignment>().last()
val call = assignment.value as? FunctionCallExpression
withClue("expected the ${expectedFuncPrefix}() call to survive, got: ${assignment.value}") {
call shouldNotBe null
}
withClue("expected a ${expectedFuncPrefix}__* builtin call") {
call!!.target.nameInSource.single() shouldStartWith expectedFuncPrefix
}
}
test("min/max assignment where the target aliases an operand keeps the comparison") {
// The rewrite of "x = min(expr, c)" into "x = expr; if x > c then x = c" assigns to x
// before doing the comparison, so it is only valid when the compared operand is a real
// constant. When it is a variable that aliases x, the comparison reads the already
// overwritten value and the min/max silently degenerates into plain "x = expr".
// See github issue #247.
assertNotReducedToOneOperand("""
main {
word @shared err
word @shared speed
sub start() {
err = 80
speed = 1
speed = min(err / 8, speed)
}
}""", "min")
}
test("min/max split into assign+compare is still applied for constant operands") {
val src = """
main {
word @shared x
sub start() {
x = 5
x = min(x, 100)
}
}"""
val result = compileText(Cx16Target(), optimize = true, src, outputDir, writeAssembly = false)!!
val stmts = result.compilerAst.entrypoint.statements
stmts.any { it is IfElse } shouldBe true
}
test("min/max keeps comparison when target aliases the second operand") {
assertNotReducedToOneOperand("""
main {
word @shared err
word @shared speed
sub start() {
err = 80
speed = 1
speed = min(err / 8, speed)
}
}""", "min")
}
test("min/max keeps comparison when target aliases the first operand") {
assertNotReducedToOneOperand("""
main {
word @shared err
word @shared speed
sub start() {
err = 80
speed = 1
speed = min(speed, err / 8)
}
}""", "min")
}
test("max keeps comparison when target aliases the second operand") {
assertNotReducedToOneOperand("""
main {
word @shared err
word @shared speed
sub start() {
err = -80
speed = -1
speed = max(err / 8, speed)
}
}""", "max")
}
test("max keeps comparison when target aliases the first operand") {
assertNotReducedToOneOperand("""
main {
word @shared err
word @shared speed
sub start() {
err = -80
speed = -1
speed = max(speed, err / 8)
}
}""", "max")
}
test("pointer indexing inside other expression ok") {
val src="""
main{
+154
View File
@@ -2823,4 +2823,158 @@ main {
result shouldNotBe null
}
}
test("parenthesized pointer field access desugars same as unparenthesized") {
val src="""
main {
struct Thing {
uword port
^^Thing next
bool flag
}
sub start() {
^^Thing @shared t
bool @shared d1 = t.next.flag
bool @shared d2 = (t.next).flag
ubyte @shared d3 = t.next.flag as ubyte
ubyte @shared d4 = (t.next).flag as ubyte
bool @shared d5 = t.next.next.flag
bool @shared d6 = (t.next).next.flag
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!!
val assigns = result.compilerAst.entrypoint.statements.filterIsInstance<Assignment>()
.associateBy { it.target.identifier!!.nameInSource.last() }
val d1 = assigns.getValue("d1").value as PtrDereference
d1.chain shouldBe listOf("t", "next", "flag")
(assigns.getValue("d2").value as PtrDereference).chain shouldBe d1.chain
((assigns.getValue("d3").value as TypecastExpression).expression as PtrDereference).chain shouldBe d1.chain
((assigns.getValue("d4").value as TypecastExpression).expression as PtrDereference).chain shouldBe d1.chain
val d5 = assigns.getValue("d5").value as PtrDereference
d5.chain shouldBe listOf("t", "next", "next", "flag")
(assigns.getValue("d6").value as PtrDereference).chain shouldBe d5.chain
compileText(VMTarget(), true, src, outputDir) shouldNotBe null
compileText(C64Target(), false, src, outputDir) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir) shouldNotBe null
}
test("field access on casted struct pointer expression") {
val src="""
main {
struct Thing {
uword port
^^Thing next
bool flag
}
sub start() {
^^Thing @shared t
bool @shared d1 = (t.port as ^^Thing).flag
ubyte @shared d2 = (t.port as ^^Thing).flag as ubyte
^^Thing @shared d3 = (t.port as ^^Thing).next
bool @shared d4 = (t.port as ^^Thing).next.flag
bool @shared d5 = ((t.port as ^^Thing).next).flag
}
}"""
val errors = ErrorReporterForTests()
val result = compileText(VMTarget(), false, src, outputDir, errors, writeAssembly = false)
withClue(errors.errors.joinToString("\n")) {
result shouldNotBe null
}
val assigns = result!!.compilerAst.entrypoint.statements.filterIsInstance<Assignment>()
.associateBy { it.target.identifier!!.nameInSource.last() }
// (t.port as ^^Thing).field --> peekXXX((t.port as ^^Thing as uword) + offsetof(field))
val d1 = assigns.getValue("d1").value as FunctionCallExpression
d1.target.nameInSource shouldBe listOf("peekbool")
val d1addr = d1.args.single() as BinaryExpression
d1addr.operator shouldBe "+"
(d1addr.right as NumericLiteral).number shouldBe 4.0 // offsetof(Thing.flag)
val d2 = assigns.getValue("d2").value as TypecastExpression
d2.type shouldBe DataType.UBYTE
(d2.expression as FunctionCallExpression).target.nameInSource shouldBe listOf("peekbool")
// pointer-typed field read: peekw + cast back to the pointer type
val d3 = assigns.getValue("d3").value as TypecastExpression
d3.type.isPointer shouldBe true
val d3peek = d3.expression as FunctionCallExpression
d3peek.target.nameInSource shouldBe listOf("peekw")
((d3peek.args.single() as BinaryExpression).right as NumericLiteral).number shouldBe 2.0 // offsetof(Thing.next)
// chained: inner pointer-typed field is read via peekw first, then .flag via peekbool
val d4 = assigns.getValue("d4").value as FunctionCallExpression
d4.target.nameInSource shouldBe listOf("peekbool")
val d4addr = d4.args.single() as BinaryExpression
(d4addr.right as NumericLiteral).number shouldBe 4.0
val d4inner = (d4addr.left as TypecastExpression).expression as TypecastExpression
(d4inner.expression as FunctionCallExpression).target.nameInSource shouldBe listOf("peekw")
// explicit parenthesized chain desugars the same
(assigns.getValue("d5").value as FunctionCallExpression).target.nameInSource shouldBe listOf("peekbool")
compileText(VMTarget(), true, src, outputDir) shouldNotBe null
compileText(C64Target(), false, src, outputDir) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir) shouldNotBe null
}
xtest("function call returning pointer - field access chaining as assignment target [nasty parser problem]") {
val src="""
main {
struct Thing {
uword port
^^Thing next
bool flag
}
sub getThing() -> ^^Thing {
return 0
}
sub start() {
getThing().flag = true
getThing().next.flag = true
getThing().next.next.flag = false
getThing().port = 1234
getThing().next.port = 5678
}
}"""
// currently fails at parse stage, should eventually compile for both VM and 6502 without needing assembly generation
compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null
}
test("function call returning pointer - field access chaining as value (RHS) works") {
val src="""
main {
struct Thing {
uword port
^^Thing next
bool flag
}
sub getThing() -> ^^Thing {
return 0
}
sub start() {
bool a = getThing().flag
bool b = getThing().next.flag
bool c = getThing().next.next.flag
uword d = getThing().port
uword e = getThing().next.port
bool f = getThing().flag and getThing().next.flag
uword g = getThing().port + getThing().next.port
if getThing().flag {
a = true
}
}
}"""
// RHS value case already works for both VM and 6502 without needing assembly generation
compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
compileText(Cx16Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null
}
})
+26
View File
@@ -1063,4 +1063,30 @@ main {
compileText(C64Target(), false, src, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("typecast of array element as left operand of shift into long target doesn't crash codegen") {
val text = """
main {
sub start() {
word[8] @shared rotatedx
ubyte @shared p1 = 0
long @shared x = rotatedx[p1] as long >> 7
}
}"""
compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), true, text, outputDir, writeAssembly = true) shouldNotBe null
}
test("typecast of array element as left operand of long addition into long target doesn't crash codegen") {
val text = """
main {
sub start() {
word[8] @shared rotatedz
ubyte @shared p1 = 0
long @shared x = (rotatedz[p1] as long + 43520) >> 7
}
}"""
compileText(C64Target(), true, text, outputDir, writeAssembly = true) shouldNotBe null
compileText(VMTarget(), true, text, outputDir, writeAssembly = true) shouldNotBe null
}
})
+149
View File
@@ -1,5 +1,6 @@
package prog8tests.ast
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.nulls.shouldNotBeNull
@@ -694,4 +695,152 @@ main {
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
errors.printedErrors.any { it.contains("2D arrays are not allowed as struct fields") } shouldBe true
}
test("struct field array size can be const expression") {
val src = """
main {
const ubyte SIZE = 5
struct Node {
ubyte[SIZE] data
uword top
}
sub start() {
^^Node n = [ [1,2,3,4,5], 9999 ]
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors, false)
errors.errors shouldBe emptyList()
}
test("struct field array size can be folded constant expression") {
val src = """
main {
const ubyte BASE = 3
struct Node {
ubyte[BASE*2] data
}
sub start() {
^^Node n = [ [1,2,3,4,5,6] ]
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors, false)
errors.errors shouldBe emptyList()
}
test("struct field array size rejects non-constant expression") {
val src = """
main {
uword dyn
struct Node {
uword[dyn] data
}
sub start() {
dyn = 5
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
errors.errors.any { it.contains("array length must be known at compile-time") } shouldBe true
}
test("struct field array size rejects zero size") {
val src = """
main {
const ubyte SIZE = 0
struct Node {
ubyte[SIZE] data
}
sub start() { }
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
errors.errors.any { it.contains("array length must be positive") } shouldBe true
}
test("variable declaration followed by statement on same line should give error") {
val src = """
main {
sub start() {
ubyte length foo(42)
}
sub foo(ubyte x) {
x++
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "must be on its own line"
}
test("variable declaration followed by statement on next line is fine") {
val src = """
main {
sub start() {
ubyte length
length = 42
foo(length)
}
sub foo(ubyte x) {
x++
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors, false) shouldNotBe null
errors.errors.size shouldBe 0
}
test("two statements on same line is fine if first one is not a variable declaration") {
val src = """
main {
sub start() {
foo(1) foo(2)
}
sub foo(ubyte x) {
cx16.r0 = x
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors, false) shouldNotBe null
errors.errors.size shouldBe 0
}
test("boolean expressions mixing not with or/and compile") {
// the 'not' simplification and the De Morgan rewrite of the same subtree used to both queue an
// ast modification, which corrupted the parent links of the nodes they shared (compiler crash)
val conditions = listOf(
"a == b or not (a >= b)",
"not (a >= b) or not (c >= b)",
"not (a >= b) or c == b",
"not (a == 0) and b == 0",
"a == b and not (a >= b)",
"(a == b) == 0 or not (c >= d)"
)
conditions.forEach { condition ->
val src = """
main {
sub start() {
uword a,b,c,d
if $condition {
a++
}
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(Cx16Target(), true, src, outputDir, errors, false) shouldNotBe null
withClue(condition) {
errors.errors.size shouldBe 0
}
}
}
})
+148
View File
@@ -664,6 +664,61 @@ main {
}
test("alias to enum member works") {
val src = """
main {
enum Priority { LOW, NORMAL, HIGH, EXTREME }
alias myNormal = Priority::NORMAL
alias chainedNormal = myNormal
sub start() {
ubyte @shared value1 = Priority::NORMAL
ubyte @shared value2 = myNormal
ubyte @shared value3 = chainedNormal
}
}"""
val result = compileText(Cx16Target(), false, src, outputDir, writeAssembly = false)!!
val start = result.compilerAst.allBlocks.first { it.name == "main" }.statements.filterIsInstance<prog8.ast.statements.Subroutine>().first { it.name == "start" }
val assignments = start.statements.filterIsInstance<Assignment>()
val value1 = (assignments[0].value as? NumericLiteral)?.number
val value2 = (assignments[1].value as? NumericLiteral)?.number
val value3 = (assignments[2].value as? NumericLiteral)?.number
value1 shouldBe 1.0
value2 shouldBe 1.0
value3 shouldBe 1.0
}
test("alias to whole enum gives error") {
val src = """
main {
enum Priority { LOW, NORMAL, HIGH, EXTREME }
alias myEnum = Priority
sub start() {
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "cannot alias an enum"
}
test("alias to nonexistent enum member gives undefined error") {
val src = """
main {
enum Priority { LOW, NORMAL, HIGH, EXTREME }
alias myBad = Priority::NONEXISTENT
sub start() {
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "undefined symbol: Priority::NONEXISTENT"
}
test("double colon in name only allowed in desugared enum members 2") {
val src = """
main {
@@ -875,5 +930,98 @@ main {
initValue.type shouldBe BaseDataType.LONG
initValue.number.toInt() shouldBe 0x00200204
}
test("mixed type bitwise operations promote to widest type") {
// Regression test: ubyte | long was truncated to ubyte instead of promoting to long.
// This caused MEMF_CHIP (ubyte) | MEMF_CLEAR (long) to lose the MEMF_CLEAR flag.
val src = $$"""
main {
sub start() {
const ubyte SMALL = $02
const long BIG = $00010000
long @shared result_or = SMALL | BIG
long @shared result_and = (SMALL | BIG) & $00010002
long @shared result_xor = SMALL ^ BIG
}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false, errors = ErrorReporterForTests())!!
val st = result.compilerAst.entrypoint.statements
val orAssign = st.filterIsInstance<Assignment>().find {
(it.target.toExpression() as? IdentifierReference)?.nameInSource?.lastOrNull() == "result_or"
}
orAssign shouldNotBe null
val orValue = orAssign!!.value as NumericLiteral
orValue.type shouldBe BaseDataType.LONG
orValue.number.toInt() shouldBe 0x00010002
val andAssign = st.filterIsInstance<Assignment>().find {
(it.target.toExpression() as? IdentifierReference)?.nameInSource?.lastOrNull() == "result_and"
}
andAssign shouldNotBe null
val andValue = andAssign!!.value as NumericLiteral
andValue.type shouldBe BaseDataType.LONG
andValue.number.toInt() shouldBe 0x00010002
val xorAssign = st.filterIsInstance<Assignment>().find {
(it.target.toExpression() as? IdentifierReference)?.nameInSource?.lastOrNull() == "result_xor"
}
xorAssign shouldNotBe null
val xorValue = xorAssign!!.value as NumericLiteral
xorValue.type shouldBe BaseDataType.LONG
xorValue.number.toInt() shouldBe 0x00010002
}
// TODO this test should be enabled again once new-codegen branch is merged
xtest("untyped const integer defaults to long") {
val src = """
main {
const A = 42
sub start() {}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!!
val st = result.compilerAst.allBlocks.first { it.name=="main" }.statements
val decl = st[0] as VarDecl
decl.datatype.base shouldBe BaseDataType.LONG
(decl.value as NumericLiteral).number shouldBe 42.0
}
// TODO this test should be enabled again once new-codegen branch is merged
xtest("untyped const float infers float type") {
val src = """
main {
const PI = 3.14
sub start() {}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!!
val st = result.compilerAst.allBlocks.first { it.name=="main" }.statements
val decl = st[0] as VarDecl
decl.datatype.base shouldBe BaseDataType.FLOAT
(decl.value as NumericLiteral).number shouldBe 3.14
}
// TODO this test should be enabled again once new-codegen branch is merged
xtest("untyped const bool infers bool type") {
val src = """
main {
const FLAG = true
sub start() {}
}"""
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!!
val st = result.compilerAst.allBlocks.first { it.name=="main" }.statements
val decl = st[0] as VarDecl
decl.datatype.base shouldBe BaseDataType.BOOL
(decl.value as NumericLiteral).number shouldBe 1.0
}
// TODO this test should be enabled again once new-codegen branch is merged
xtest("untyped const memory() infers pointer type") {
val src = """
main {
const M = memory("screen", 1000, 0)
sub start() {}
}"""
compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null
}
})
+194
View File
@@ -298,6 +298,167 @@ testblock {
compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly=false, errors=errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("alias to label") {
val src="""
main {
sub start() {
alias target2 = target
goto target2
cx16.r0 = 666
target:
cx16.r1 = 42
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly=false) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly=false) shouldNotBe null
}
test("alias to builtin function") {
val src="""
main {
sub start() {
alias pw = peekw
alias pb = peek
cx16.r0 = pw($2000)
cx16.r1 = pb($2001)
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly=false) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly=false) shouldNotBe null
}
test("alias to array supports indexing") {
val src="""
main {
sub start() {
alias a = other.arr
a[0] = 42
a[1] = 99
cx16.r0 = a[1]
}
}
other {
ubyte[4] @shared arr
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly=false) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly=false) shouldNotBe null
}
test("private alias cannot be accessed from outside its block") {
val src="""
main {
sub start() {
helper.myReg = 42
}
}
helper {
private alias myReg = cx16.r0
}"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, writeAssembly=false, errors=errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "cannot access private alias 'myReg' from outside its block"
}
test("alias name conflict with existing symbol gives error") {
val src="""
main {
sub start() {
ubyte x
alias x = cx16.r0
x = 1
}
}"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, writeAssembly=false, errors=errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "name conflict 'x'"
}
test("alias to block: calling subroutine through it") {
val src="""
main {
sub start() {
alias gfx = gfx_hires
gfx.graphics_mode()
gfx.somevar = 42
cx16.r0 = gfx.somevar
cx16.r1 = gfx.MODE
}
}
gfx_hires {
ubyte @shared somevar
const ubyte MODE = 2
sub graphics_mode() {
cx16.r2++
}
}"""
val errors = ErrorReporterForTests()
compileText(C64Target(), false, src, outputDir, writeAssembly=false, errors=errors) shouldNotBe null
errors.errors.size shouldBe 0
errors.clear()
compileText(VMTarget(), false, src, outputDir, writeAssembly=false, errors=errors) shouldNotBe null
errors.errors.size shouldBe 0
}
test("alias to block declared in block scope used in subroutine") {
val src="""
main {
alias gfx = gfx_hires
sub start() {
gfx.graphics_mode()
}
}
gfx_hires {
sub graphics_mode() {
cx16.r0++
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly=false) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly=false) shouldNotBe null
}
test("alias to block: calling nested subroutine through it") {
val src="""
main {
sub start() {
alias tb = testblock
tb.sub1.sub2()
}
}
testblock {
sub sub1() {
sub sub2() {
cx16.r1++
}
}
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly=false) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly=false) shouldNotBe null
}
test("alias to block: array and struct member access through it") {
val src="""
main {
sub start() {
alias gfx = gfx_hires
gfx.arr[0] = 7
gfx.node.value = 9
cx16.r0 = gfx.arr[0]
cx16.r1 = gfx.node.value
}
}
gfx_hires {
ubyte[4] @shared arr
struct S {
ubyte value
}
^^S @shared node
}"""
compileText(C64Target(), false, src, outputDir, writeAssembly=false) shouldNotBe null
compileText(VMTarget(), false, src, outputDir, writeAssembly=false) shouldNotBe null
}
}
context("strings") {
@@ -1399,6 +1560,39 @@ main {
errors.errors[3] shouldContain "builtin function cannot be redefined"
}
test("block names cannot be builtin functions or keywords") {
val src="""
main {
sub start() {
}
}
private {
sub foo() -> uword {
return 1
}
}
memory {
sub foo() -> uword {
return 1
}
}
mkword {
sub foo() -> uword {
return 2
}
}
"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), optimize=false, src, outputDir, errors=errors) shouldBe null
errors.errors.size shouldBe 3
errors.errors[0] shouldContain "name cannot be a keyword"
errors.errors[1] shouldContain "builtin function cannot be redefined"
errors.errors[2] shouldContain "builtin function cannot be redefined"
}
test("string and array multiplication require integer multiplicand") {
val src="""
main {
@@ -62,6 +62,116 @@ class TestExecution6502 : FunSpec({
machine.assertMemory(0x02, 42)
}
test("indexed pointer fields in structs") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
%zeropage basicsafe
main {
struct Node {
bool flag
^^uword uptr
}
struct Wrapper {
bool z
^^Node node
}
^^Node @shared n = [true, 4000]
^^Node @shared inner = [true, 4500]
^^Wrapper @shared w = [false, 4000]
&ubyte poweroff = $f203
&uword slot_low = $0300
&uword slot_hi = $0302
uword index
sub start() {
n.uptr[3] = 9999
n.uptr[1] = 7777
n.uptr[9] = 5555
slot_low = n.uptr[3]
index = 1
n.uptr[index] = 1111
slot_hi = n.uptr[1]
w.node = inner
w.node.uptr[2] = 1234
poweroff = 1
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
val machine = compileResult!!.simulate()
machine.assertMemory(0x0300, 9999 and 0xff)
machine.assertMemory(0x0301, 9999 shr 8)
machine.assertMemory(0x0302, 1111 and 0xff)
machine.assertMemory(0x0303, 1111 shr 8)
machine.assertMemory(4000+6, 9999 and 0xff)
machine.assertMemory(4000+7, 9999 shr 8)
machine.assertMemory(4000+2, 1111 and 0xff)
machine.assertMemory(4000+3, 1111 shr 8)
machine.assertMemory(4000+18, 5555 and 0xff)
machine.assertMemory(4000+19, 5555 shr 8)
machine.assertMemory(4500+4, 1234 and 0xff)
machine.assertMemory(4500+5, 1234 shr 8)
}
test("more struct pointer field operations") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
%zeropage basicsafe
main {
struct Node {
bool flag
^^uword uptr
}
^^Node @shared n = [true, 4000]
^^Node @shared m = [true, 5000]
^^uword @shared normalptr = 4000
&ubyte poweroff = $f203
&uword n_after_swap = $0300
&uword m_after_swap = $0302
&uword n_after_add = $0304
&uword lsb_result = $0306
&uword msb_result = $0308
&uword idxinc_result = $030A
sub start() {
swap(n.uptr, m.uptr)
n_after_swap = n.uptr
m_after_swap = m.uptr
n.uptr += 2
n_after_add = n.uptr
n.uptr[3] = 100
n.uptr[3]++
idxinc_result = n.uptr[3]
lsb_result = lsb(normalptr)
msb_result = msb(normalptr)
poweroff = 1
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
val machine = compileResult!!.simulate()
machine.assertMemory(0x0300, 5000 and 0xff)
machine.assertMemory(0x0301, 5000 shr 8)
machine.assertMemory(0x0302, 4000 and 0xff)
machine.assertMemory(0x0303, 4000 shr 8)
machine.assertMemory(0x0304, 5004 and 0xff)
machine.assertMemory(0x0305, 5004 shr 8)
machine.assertMemory(0x0306, 160)
machine.assertMemory(0x0308, 15)
machine.assertMemory(0x030A, 101)
machine.assertMemory(0x030B, 0)
machine.assertMemory(5010, 101)
machine.assertMemory(5011, 0)
}
test("serial output") {
val src = $$"""
%encoding iso
@@ -379,6 +489,76 @@ class TestExecution6502 : FunSpec({
machine.assertMemory(0x2003, 0x00)
}
test("double increment/decrement of uword struct field via typed pointer") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
&uword result = $02
struct Sprite {
ubyte pad
ubyte pad2
uword x
}
^^Sprite[4] sprites
sub start() {
^^Sprite sprite = sprites[0]
sprite.x = 255
sprite.x++
sprite.x++
result = sprite.x ; expect $0101
poweroff = 1
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
var machine = compileResult!!.simulate()
machine.assertMemory(0x02, 0x01)
machine.assertMemory(0x03, 0x01)
val src2 = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
&uword result = $02
struct Sprite {
ubyte pad
ubyte pad2
uword x
}
^^Sprite[4] sprites
sub start() {
^^Sprite sprite = sprites[0]
sprite.x = 1
sprite.x--
sprite.x--
result = sprite.x ; expect $ffff
poweroff = 1
}
}
""".trimIndent()
val compileResult2 = compileText(Cx16Target(), false, src2, outputDir)
machine = compileResult2!!.simulate()
machine.assertMemory(0x02, 0xff)
machine.assertMemory(0x03, 0xff)
}
test("struct with inlined array memory layout (6502)") {
val src = $$"""
%option no_sysinit
@@ -414,4 +594,69 @@ class TestExecution6502 : FunSpec({
machine.assertMemory(0x4007, 9999 and 0xff)
machine.assertMemory(0x4008, (9999 shr 8) and 0xff)
}
test("lsb/msb/lsw/msw result delivered to a cx16 virtual register parameter") {
val src = $$"""
%option no_sysinit
%launcher none
%address $1000
main {
&ubyte poweroff = $f203
; each routine copies the value it received in the @R0 virtual register to a probe address
asmsub recordLsb(ubyte c @R0) {
%asm {{
lda cx16.r0L
sta $3000
rts
}}
}
asmsub recordMsb(ubyte c @R0) {
%asm {{
lda cx16.r0L
sta $3001
rts
}}
}
asmsub recordLsw(uword c @R0) {
%asm {{
lda cx16.r0
sta $3002
lda cx16.r0+1
sta $3003
rts
}}
}
asmsub recordMsw(uword c @R0) {
%asm {{
lda cx16.r0
sta $3004
lda cx16.r0+1
sta $3005
rts
}}
}
sub start() {
uword @shared l
l = $1234
recordLsb(lsb(l))
recordMsb(msb(l))
recordLsw(lsw(l))
recordMsw(msw(l))
poweroff = 1
}
}
""".trimIndent()
val compileResult = compileText(Cx16Target(), false, src, outputDir)
val machine = compileResult!!.simulate()
machine.assertMemory(0x3000, 0x34) // lsb($1234)
machine.assertMemory(0x3001, 0x12) // msb($1234)
machine.assertMemory(0x3002, 0x34) // lsw($1234) low
machine.assertMemory(0x3003, 0x12) // lsw($1234) high
machine.assertMemory(0x3004, 0x00) // msw($1234) low
machine.assertMemory(0x3005, 0x00) // msw($1234) high
}
})
@@ -643,6 +643,54 @@ main {
lines.any { it.startsWith("jsr") && it.contains("p8s_test") } shouldBe true
}
test("inline asm pipe operator is preserved as bitwise OR") {
val src="""
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
%asm {{
lda #%00010000 | $1234>>16
}}
}
}"""
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
val lines = asm.lines().map { it.trim() }
// the '|' must remain on the same line as the lda, not be split into a second line
lines.any { it == "lda #%00010000 | $1234>>16" } shouldBe true
lines.any { it == "$1234>>16" } shouldBe false
}
test("subroutine with register-mapped parameter is not inlined") {
val src= $$"""
%zeropage basicsafe
%option no_sysinit
main {
sub start() {
PlayVideo_STD(mkword($10, $20))
}
sub PlayVideo_STD(uword params @R0) {
TrampolineCallSTD($a000)
}
asmsub TrampolineCallSTD(uword addr @AY) clobbers(A, Y) {
%asm {{
rts
}}
}
}"""
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
val lines = asm.lines().map { it.trim() }
// the R0 argument setup must be preserved because the inlined body doesn't
// explicitly reference the register-mapped parameter
lines.any { it == "sta cx16.r0" } shouldBe true
lines.any { it == "sty cx16.r0+1" } shouldBe true
lines.any { it.startsWith("jsr") && it.contains("p8s_PlayVideo_STD") } shouldBe true
}
test("if not without else is not swapped") {
val src="""
main {
+112 -1
View File
@@ -66,7 +66,7 @@ main {
}
test("taking address of split arrays works") {
val src="""
val src = """
main {
sub start() {
cx16.r0L=0
@@ -486,6 +486,104 @@ main {
cmpiInstr shouldBe null
}
test("short-circuit boolean and/or always emits explicit CMPI before the branch (strict status-bits contract)") {
val src = """
main {
uword aa
uword bb
sub start() {
aa = 5
bb = 9
bool @shared res = aa == bb or aa < bb
bool @shared res2 = aa == bb and aa < bb
}
}"""
val result = compileText(VMTarget(), optimize=false, src, outputDir, writeAssembly=true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
val irProgram = IRFileReader().read(virtfile)
val start = irProgram.blocks[0].children[0] as IRSubroutine
val instructions = start.chunks.flatMap { c->c.instructions }
// Both short-circuit operators must test the left operand with an explicit CMPI #0 before
// branching; without it the branch reads the status bits of whatever instruction ran before,
// which gave wrong results. A BST that directly follows a CMP is the comparison itself
// materialising its result, that one is fine.
// See CpuType.statusBitsOnMultiByteOps for the rationale.
val branches = instructions.withIndex().filter { it.value.opcode == Opcode.BSTEQ || it.value.opcode == Opcode.BSTNE }
withClue("branch instructions found") {
branches.size shouldBeGreaterThan 0
}
branches.forEach { (idx, branch) ->
val before = instructions[idx - 1]
withClue("instruction before $branch") {
val testedByCmp = before.opcode == Opcode.CMP
val testedByCmpi = before.opcode == Opcode.CMPI && before.immediate == 0
testedByCmp || testedByCmpi shouldBe true
}
}
}
test("short-circuit boolean and/or compute the correct value in the vm") {
val src = """
main {
uword a
uword b
bool p
sub start() {
; every case adds its own bit to cx16.r0 when the expression is true,
; so a wrong result shows up as a difference in the bit pattern
cx16.r0 = 0
a = 5
b = 5
bool t = a == b or a < b ; true
if t
cx16.r0 += 1
a = 5
b = 9
t = a == b or a < b ; true
if t
cx16.r0 += 2
a = 9
b = 5
t = a == b or a < b ; false
if t
cx16.r0 += 4
a = 1
b = 5
t = a == b and a < b ; false
if t
cx16.r0 += 8
a = 9
b = 5
t = a == b and a < b ; false
if t
cx16.r0 += 16
; a bool variable as left operand: no comparison runs before the branch
p = false
a = 5
b = 9
t = p or a < b ; true
if t
cx16.r0 += 32
p = true
t = p and a < b ; true
if t
cx16.r0 += 64
a = 9
b = 5
t = p and a < b ; false
if t
cx16.r0 += 128
}
}"""
val result = compileText(VMTarget(), optimize=true, src, outputDir, writeAssembly=true)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runAndTestProgram(virtfile.readText()) { vm ->
// bits 1, 2, 32 and 64 must be set: 1 + 2 + 32 + 64
vm.memory.getUW(0xff02u) shouldBe 99u
}
}
test("repeat counts (const)") {
val src="""
main {
@@ -647,6 +745,19 @@ main {
instructions[10].type shouldBe IRDataType.WORD
}
test("word pop comparison uses the word result type") {
val src=$$"""
main {
sub start() {
ubyte a
if popw() != $abcd {
a++
}
}
}"""
compileText(VMTarget(), false, src, outputDir, writeAssembly = true) shouldNotBe null
}
test("typed address-of a const pointer with non-const array indexing") {
val src= """
main {
@@ -194,8 +194,13 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
output("private ")
outputln("struct ${struct.name} {")
for(member in struct.fields) {
val arraySuffix = if(member.isArray) "[${member.arraySize}]" else ""
outputlni( " ${member.type}$arraySuffix ${member.name}")
outputi(" ${member.type}")
if(member.isArray) {
output("[")
member.arraySize?.indexExpr?.accept(this)
output("]")
}
outputln(" ${member.name}")
}
outputlni("}")
}
@@ -814,11 +814,11 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
override fun visitStructdeclaration(ctx: StructdeclarationContext): StructDecl {
val name = getname(ctx.identifier())
val fieldDefs = ctx.structfielddecl().map { getStructField(it) }
val flattened = fieldDefs.flatMap { (dt, names, arrSize) -> names.map { StructField(dt, it, arrSize) }}
val flattened = fieldDefs.flatMap { (dt, names, arrSize) -> names.map { StructField(dt, it, arrSize?.copy()) }}
return StructDecl(name, flattened.toTypedArray(), ctx.PRIVATE() != null, ctx.toPosition())
}
private data class StructFieldDef(val type: DataType, val names: List<String>, val arraySize: Int?)
private data class StructFieldDef(val type: DataType, val names: List<String>, val arraySize: ArrayIndex?)
private fun getStructField(ctx: StructfielddeclContext): StructFieldDef {
val identifiers = ctx.identifierlist()?.identifier() ?: emptyList()
@@ -833,9 +833,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
throw SyntaxError("2D arrays are not allowed as struct fields", ctx.toPosition())
val dt = baseDt.elementToArray()
val arrayIndexExpr = arrayIndices[0].accept(this) as ArrayIndex
val size = (arrayIndexExpr.indexExpr as? NumericLiteral)?.number?.toInt()
?: throw SyntaxError("array field size must be a constant integer expression", ctx.toPosition())
dt to size
dt to arrayIndexExpr
} else {
baseDt to null
}
@@ -276,7 +276,11 @@ class BinaryExpression(
// SOMEEXPRESSION . NAME
val leftDt = leftExpr.inferType(program)
if (leftDt.isPointer) leftDt.getOrUndef().subType else null
} else null
} else {
// generic pointer expression . field (e.g. (expr as ^^Struct).field, or (ptrNext).field)
val leftDt = left.inferType(program)
if (leftDt.isPointer) leftDt.getOrUndef().subType else null
}
if (struct == null)
InferredTypes.unknown()
else {
@@ -517,20 +517,37 @@ class StructDecl(override val name: String, val fields: Array<StructField>, val
override fun linkParents(parent: Node) {
this.parent = parent
fields.forEach { it.arraySize?.linkParents(this) }
}
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
override fun referencesIdentifier(nameInSource: List<String>) = false
override fun copy() = StructDecl(name, fields.clone(), isPrivate, position)
override fun replaceChildNode(node: Node, replacement: Node) {
require(replacement is ArrayIndex)
val field = fields.firstOrNull { it.arraySize === node }
?: throw FatalAstException("invalid replace in $this at ${node.position}: $node -> $replacement")
field.arraySize = replacement
replacement.linkParents(this)
}
override fun referencesIdentifier(nameInSource: List<String>) = fields.any { it.arraySize?.referencesIdentifier(nameInSource)==true }
override fun copy() = StructDecl(name, fields.map { it.copy() }.toTypedArray(), isPrivate, position)
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
override fun memsize(sizer: IMemSizer): Int = fields.sumOf { field ->
if(field.isArray)
sizer.memorySize(field.type, field.arraySize!!)
sizer.memorySize(field.type, field.constSize() ?: 1)
else
sizer.memorySize(field.type, 1)
}
override fun sameas(other: ISubType): Boolean = other is StructDecl && other.name==name && other.fields.contentEquals(fields)
override fun sameas(other: ISubType): Boolean {
if(other !is StructDecl || other.name!=name || other.fields.size!=fields.size)
return false
return fields.zip(other.fields).all { (a, b) ->
a.name==b.name && a.type==b.type && when {
a.arraySize==null && b.arraySize==null -> true
a.arraySize!=null && b.arraySize!=null -> a.arraySize!! isSameAs b.arraySize!!
else -> false
}
}
}
override fun getFieldType(name: String): DataType? = fields.firstOrNull { it.name==name }?.type
fun getField(name: String): StructField? = fields.firstOrNull { it.name==name }
@@ -540,7 +557,7 @@ class StructDecl(override val name: String, val fields: Array<StructField>, val
fields.fold(0) { offset, field ->
if (field.name == fieldname)
return offset.toUByte()
val numElements = if(field.isArray) field.arraySize!! else 1
val numElements = if(field.isArray) field.constSize() ?: 1 else 1
offset + sizer.memorySize(field.type, numElements)
}
return null
@@ -548,8 +565,10 @@ class StructDecl(override val name: String, val fields: Array<StructField>, val
}
data class StructField(val type: DataType, val name: String, val arraySize: Int? = null) {
data class StructField(val type: DataType, val name: String, var arraySize: ArrayIndex? = null) {
val isArray: Boolean get() = arraySize != null
fun constSize(): Int? = arraySize?.constIndex()
fun copy(): StructField = StructField(type, name, arraySize?.copy())
}
+70 -2
View File
@@ -4,6 +4,8 @@ import prog8.ast.*
import prog8.ast.expressions.*
import prog8.ast.statements.*
import prog8.code.core.CommutativeOperators
import java.util.Collections
import java.util.IdentityHashMap
/**
@@ -26,6 +28,15 @@ open class AstModification {
/** Nodes that will be affected when [perform] is called. */
open val affectedNodes: List<Node> = emptyList()
/**
* The node in which a child is modified by [perform], if there is one.
* Modifications whose target parent node (or a node higher up in its parent chain) is no
* longer part of the tree are skipped by [AstWalker.applyModifications]: they would only
* change a subtree that has been detached by another modification, and re-linking the
* parents of nodes that are still in use elsewhere corrupts the tree.
*/
open val targetParent: Node? = null
/** Apply the modification to the AST. */
open fun perform() {}
}
@@ -41,6 +52,7 @@ open class AstRemove(val node: Node, val parent: IStatementContainer) : AstModif
}
override val affectedNodes = listOf(node)
override val targetParent: Node get() = parent as Node
override fun perform() {
if (!parent.statements.remove(node)) {
@@ -60,6 +72,7 @@ open class AstSetExpression(
private val parent: Node
) : AstModification() {
override val affectedNodes = listOf(newExpr)
override val targetParent: Node get() = parent
override fun perform() {
setter(newExpr)
@@ -77,6 +90,7 @@ open class AstInsert private constructor(
private val position: InsertPosition
) : AstModification() {
override val affectedNodes = listOf(stmt)
override val targetParent: Node get() = parent as Node
override fun perform() {
val idx = when (position) {
@@ -113,6 +127,7 @@ open class AstReplaceNode(
private val parent: Node
) : AstModification() {
override val affectedNodes = listOf(node, replacement)
override val targetParent: Node get() = parent
override fun perform() {
parent.replaceChildNode(node, replacement)
@@ -273,6 +288,12 @@ abstract class AstWalker {
protected val modifications = mutableListOf<Triple<AstModification, Node, Node>>()
protected val affectedModules = mutableSetOf<Module>()
/**
* The nodes that have been replaced or removed by a modification that was applied already,
* and thus are no longer part of the tree.
*/
private val detachedNodes = Collections.newSetFromMap(IdentityHashMap<Node, Boolean>())
private fun track(mods: Iterable<AstModification>, node: Node, parent: Node) {
if (mods.any()) {
try {
@@ -293,6 +314,11 @@ abstract class AstWalker {
}
}
/**
* Applies all queued modifications, skipping modifications that would operate on a node
* or subtree that is no longer part of the tree (because an earlier modification of the
* same batch detached it). Returns the number of modifications that were performed.
*/
open fun applyModifications(): Int {
// Build conflict map for debugging - detect nodes affected by multiple modifications
val nodeToModifications = mutableMapOf<Node, MutableList<AstModification>>()
@@ -315,14 +341,55 @@ abstract class AstWalker {
}
}
detachedNodes.clear()
var amount = 0
modifications.forEach {
it.first.perform()
val mod = it.first
if (mod.operatesInDetachedSubtree()) {
// another modification of this batch already detached the subtree that this
// modification would operate in, so applying it has no effect on the tree -
// and if it reuses nodes that are still in use elsewhere, re-linking their
// parents corrupts the tree. Skip it.
return@forEach
}
mod.perform()
when (mod) {
is AstReplaceNode -> detachedNodes += mod.node
is AstRemove -> detachedNodes += mod.node
else -> { }
}
amount++
}
val amount = modifications.size
modifications.clear()
return amount
}
/** True if [node] is itself detached, or lies inside a subtree that was detached by a modification that was applied earlier in this batch. */
private fun isDetached(node: Node?): Boolean {
if (node == null || detachedNodes.isEmpty()) return false
if (node in detachedNodes) return true
var candidate = node.parent
while (candidate !is ParentSentinel) {
if (candidate in detachedNodes) return true
candidate = candidate.parent
}
return false
}
/**
* True if this modification would operate on a node or subtree that is no longer part of
* the tree: performing it would have no effect on the live tree, or could corrupt the
* parent links of nodes that are still in use by a replacement.
*/
private fun AstModification.operatesInDetachedSubtree(): Boolean {
if (isDetached(targetParent)) return true
return when (this) {
is AstReplaceNode -> isDetached(node)
is AstRemove -> isDetached(node)
else -> false
}
}
fun linkAffectedParents(namespace: Node) {
affectedModules.forEach { it.linkParents(namespace) }
affectedModules.clear()
@@ -380,6 +447,7 @@ abstract class AstWalker {
fun visit(struct: StructDecl, parent: Node) {
track(before(struct, parent), struct, parent)
struct.fields.forEach { it.arraySize?.accept(this) }
track(after(struct, parent), struct, parent)
}
@@ -45,6 +45,7 @@ interface IAstVisitor {
}
fun visit(struct: StructDecl) {
struct.fields.forEach { it.arraySize?.accept(this) }
}
fun visit(field: StructFieldRef) {
@@ -0,0 +1,23 @@
package prog8.parser
/**
* Provides the complete set of Prog8 reserved words, derived from the
* ANTLR grammar vocabulary so it never goes out of sync with the language.
*/
object Prog8Keywords {
val reservedWords: Set<String> by lazy {
val words = sortedSetOf<String>()
val vocab = Prog8ANTLRParser.VOCABULARY
for (i in 0..vocab.maxTokenType) {
val literal = vocab.getLiteralName(i) ?: continue
// literal is like "'for'" or "'^^'"; keep only word-like ones longer than 1 char
if (literal.length >= 2 && literal.startsWith("'") && literal.endsWith("'")) {
val word = literal.substring(1, literal.length - 1)
if (word.length > 1 && word.matches(Regex("[a-z][a-z_0-9]*"))) {
words.add(word)
}
}
}
words
}
}
+2 -5
View File
@@ -250,16 +250,13 @@ Major breaking changes that require code modifications when upgrading:
- **Fuzzy library search** — ``-libsearch`` now automatically attempts a fuzzy search if no exact matches are found.
**v12.3** — Not released yet
- Laying groundwork for new code generation targets. Still highly experimental.
**v12.3** — August 2026
- **Module search order rework** — replaced alphabetical search with a deterministic, prioritized list; filesystem now searched before internal libraries.
- **New experimental 6502 code generator** based on the IR, enabled with ``-newcodegen`` (do not use for production code yet).
- **m68k code generator** — a full experimental m68k code generator will follow in a later version.
- **New experimental 6502 code generator** based on the IR, enabled with ``-newcodegen`` (experimental, generates pretty bad/buggy code still!)
- **Embedded arrays in structs** — structs can now contain arrays as fields.
- **``@bank`` accepts a subroutine name** — for dynamic / overlay banking with a bank manager
- **New library routines** — ``floats.mod()``; ``sys.MAX_LONG`` / ``sys.MIN_LONG``; PET32 ``petsnd`` playback functions, ``REST`` constant, ``set_gap()``, enharmonic flat note symbols; PET32 ``sys.set_irq()`` / ``sys.restore_irq()``; serial module: configurable baud rate.
- **Breaking changes** — ``-nostdlib`` removed; ``bankmanager`` renamed to ``bankselector``; ``swap`` is now a statement.
- **Search Path Comparison**:
*This document summarizes major and minor releases. Bugfix releases (e.g., v12.0.1, v12.1.1) are omitted for brevity.*
+5 -4
View File
@@ -125,11 +125,12 @@ scoped names always need to be fully scoped (because they always start in the gl
.. index:: pair: Identifiers; Aliases
The ``alias`` statement makes it easier to refer to symbols from other places, and they can save
you from having to type the fully scoped name everytime you need to access that symbol.
you from having to type the fully scoped name every time you need to access that symbol.
Aliases can be created in any scope except at the module level.
An alias is created with ``alias <name> = <target>`` and then you can use ``<name>`` as if it were ``<target>``.
It is possible to alias variables, labels and subroutines, and even whole blocks and words
The name has to be an unscoped identifier name, the target can be any scoped or unscoped symbol.
It is possible to alias variables, labels and subroutines, and even whole blocks.
The name has to be an unscoped identifier name; the target can be any scoped or unscoped symbol,
except a whole enum (alias its members individually).
Please consider using aliases sparingly because it may lead to confusing code if you alias
well-known block names for example.
@@ -196,7 +197,7 @@ to access a variable from a nested subroutine::
}
**:index:`Aliases`** make it easier to refer to symbols from other places. They save
you from having to type the fully scoped name everytime you need to access that symbol.
you from having to type the fully scoped name every time you need to access that symbol.
Aliases can be created in any scope except at the module level.
You can create and use an alias with the ``alias`` statement like so::
+6 -2
View File
@@ -140,8 +140,12 @@ dealing with all of them separately. You first define the struct type like so::
bool elite
}
You can use boolean fields, numeric fields (byte, word, long, float), and pointer fields (including str, which is translated into ^^ubyte).
You cannot nest struct types, but inline arrays are supported as fields. 2D arrays are not allowed as struct fields.
Allowed field datatypes: all numeric types (``byte``, ``ubyte``, ``word``, ``uword``, ``long``, ``float``),
``bool``, ``str``, typed pointers to those or to other structs, and inline 1D arrays of these element types.
Not allowed: struct instances themselves (structs cannot be nested, use a pointer to the struct instead),
arrays of typed pointers such as ``^^Enemy[4]`` (use untyped pointers instead), and 2D arrays.
The typed pointer array and 2D array restrictions are limitations of the current compiler; they may or may not be lifted in a future version.
Fields in a struct are 'packed' (meaning the values are placed back-to-back in memory), and placed in memory in order of declaration. This guarantees exact size and place of the fields.
``sizeof()`` knows how to calculate the combined size of a struct, and ``offsetof()`` can be used to get the byte offset of a given field in the struct.
The size of a struct cannot exceed 1 memory page (256 bytes).
+10 -4
View File
@@ -9,27 +9,33 @@ Future Things and Ideas
- symboldump: some sort of javadocs generated from the p8 source files (instead of just the function signatures). Use markdown for formatting, not html.
- when implementing unsigned longs: remove the (multiple?) "TODO "hack" to allow unsigned long constants to be used as values for signed longs, without needing a cast
- struct/ptr: implement the remaining TODOs in PointerAssignmentsGen.
- struct/ptr: support pointer to pointer?
- struct/ptr: support arrays of typed pointers as struct fields (e.g. ``^^Enemy[4] foos``); currently rejected by AstChecker with "arrays of typed pointers are currently not supported", untyped pointers must be used instead.
- struct/ptr: support 2D arrays as struct fields; currently a parser error ("2D arrays are not allowed as struct fields"), even though 2D arrays are supported for regular variables (see variables.rst).
- struct/ptr: typed function pointers (simplified): ``&&subroutine`` returns opaque typed pointer (no ``funcptr`` keyword), assignment allowed where target type is inferred, calling via ``ptr(args)``. No explicit signature syntax. Useful mostly for 68000 targets.
- struct/ptr: really fixing the pointer dereferencing issues (cursed hybrid between IdentifierReference, PtrDereferece and PtrIndexedDereference) may require getting rid of scoped identifiers altogether and treat '.' as a "scope or pointer following operator"
- struct/ptr: (later, nasty parser problem:) support chaining pointer dereference on function calls that return a pointer. (type checking now fails on stuff like func().field and func().next.field)
- struct/ptr: (later, nasty parser problem:) support chaining pointer dereference on function calls that return a pointer as assignment target (LHS). RHS value case ``a = func().field`` / ``a = func().next.field`` now works for both VM and 6502 via generic ``peek`` desugar (``AstExpressions.kt:280``, ``CodeDesugarer.kt:654``); still fails for ``func().field = a`` / ``func().next.field = a`` which requires ``Prog8ANTLR.g4:239`` ``assign_target`` grammar change to allow ``functioncall '.' field`` chain (see ``compiler/test/TestPointers.kt`` xtest).
- Make all constants long by default? or not? (remove type name altogether), reduce to target type implicitly if the actual value fits. -> long-consts branch
This will break some existing programs that depend on value wraparound, but gives more intuitive constant number handling.
Can give descriptive error message for old syntax that still includes the type name?
- add documentation for more library modules instead of just linking to the source code
- sizeof(pointer) is now always 2 (an uword), make this a variable in the ICompilationTarget so that it could be 4 at the time we might ad a 32-bits 68000 target for example. Much code assumes word size addresses though.
- sizeof(pointer) is now always 2 (an uword), make this a variable in the ICompilationTarget so that it could be 4 at the time we might ad a 32-bits 68000 target for example. Much code assumes word size addresses though. (already done on new-codegens branch)
- add float support to the configurable compiler targets. Restrictions: just have "cbm-style floats" as an option (to that it can slot into the current float codegen), where all you have to specify is the addresses of AYINT and GIVAYF and FADDT and all their friends.
- Change scoping rules for qualified symbols so that they don't always start from the root but behave like other programming languages (look in local scope first), maybe only when qualified symbol starts with '.' such as: .local.value = 33
- implement the signed remainder byte and word routines on 6502 (virtual target already has them working)
- implement the signed divmod byte and word routines on 6502 (virtual target already has them working)
- make a form of "manual generics" possible like: varsub routine(T arg)->T where T is expanded to a specific type
(this is already done hardcoded for several of the builtin functions)
- migrate CLI argument parsing from the obsolete kotlinx-cli library to Clikt (com.github.ajalt:clikt)
- migrate CLI argument parsing from the obsolete kotlinx-cli library to Clikt (com.github.ajalt:clikt) (already done on new-codegens branch)
- add new directives ``%bssaddress`` and ``%slabsaddress`` to set the memory address for the BSS area and memory slabs (analogous to ``%address`` for program load address).
Note: these should be mutually exclusive with the existing CLI options (``-varsgolden``, ``-varshigh``, ``-slabsgolden``, ``-slabshigh``)
because the CLI options are target-aware shorthands (set bank symbols, do bounds checking against predefined ranges)
while the directives are raw addresses — they'd conflict if both specified for the same area.
- the c64 sprite multiplexer example may need timing adjustments after compiler changes (not a compiler bug — cycle-exact C64 code is inherently fragile)
- once 'new-codegen' branch is merged, re-enable bunch of xtest() in TestConst.kt
- add an ``atexit`` mechanism: like ``defer`` but runs only at program exit, regardless of which exit path is taken (fall-through, ``sys.exit``, poweroff).
Mainly useful for library-owned cleanup (heap pools, opened devices/libraries/windows on the amiga500 target) without requiring the main program to call cleanup explicitly;
``defer`` cannot do this because it is bound to subroutine scope. Prototype as a stdlib module first (fixed-capacity registry of subroutine addresses + explicit ``run_all()``, LIFO order),
consider promoting to a language feature that invokes the handlers automatically in each target's program epilogue.
Romable (%option romable)
+2 -1
View File
@@ -417,7 +417,8 @@ set it all to the same value, or maybe even simply assign the individual element
Note that the various keywords for the data type and variable type (``byte``, ``word``, ``const``, etc.)
can't be used as *identifiers* elsewhere. You can't make a variable, block or subroutine with the name ``byte``
for instance.
for instance. The same applies to the names of the builtin functions (such as ``memory``, ``mkword``,
``peek`` and ``poke``): they cannot be redefined as a variable, block, subroutine, struct or label.
Using the ``in`` operator you can easily check if a value is present in an array,
example: ``if choice in [1,2,3,4] {....}``
-55
View File
@@ -1,55 +0,0 @@
%encoding iso
%import textio
Data {
struct Record {
ubyte id
ubyte[4] data
word[2] scores
}
}
main {
sub start() {
txt.iso()
; Initialize struct instance
^^Data.Record rec = ^^Data.Record : [1, 10, 20, 30, 40, 1000, 2000]
txt.print("Record ID: ")
txt.print_ub(rec.id)
txt.print("\n")
txt.print("Data[0]: ")
txt.print_ub(rec.data[0])
txt.print("\n")
txt.print("Score[1]: ")
txt.print_w(rec.scores[1])
txt.print("\n")
; Modify array fields
rec.data[2] = 99
txt.print("Modified Data[2]: ")
txt.print_ub(rec.data[2])
txt.print("\n")
txt.print("Sizeof Record: ")
txt.print_ub(sizeof(Data.Record))
txt.print("\n")
txt.print("Offsetof id: ")
txt.print_ub(offsetof(Data.Record.id))
txt.print("\n")
txt.print("Offsetof data: ")
txt.print_ub(offsetof(Data.Record.data))
txt.print("\n")
txt.print("Offsetof scores: ")
txt.print_ub(offsetof(Data.Record.scores))
txt.print("\n")
sys.poweroff_system()
}
}
+21 -18
View File
@@ -12,10 +12,11 @@ main {
word[] ycoor = [ -100, -100, 100, 100, -100, -100, 100, 100 ]
word[] zcoor = [ -100, 100, -100, 100, -100, 100, -100, 100 ]
; storage for rotated coordinates
word[len(xcoor)] rotatedx
word[len(ycoor)] rotatedy
; storage for rotated Z (needed for sprite sort and attributes)
word[len(zcoor)] rotatedz
; precomputed screen coordinates (perspective-corrected) for drawing
ubyte[len(xcoor)] screenx
ubyte[len(ycoor)] screeny
sub start() {
@@ -68,9 +69,14 @@ main {
ubyte @zp i
for i in 0 to len(xcoor)-1 {
; don't normalize by dividing by 128, instead keep some precision for perspective calc later
rotatedx[i] = (Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i])
rotatedy[i] = (Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i])
rotatedz[i] = (Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i])
word rx = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
word ry = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
rotatedz[i] = Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i]
; perspective projection, done once per vertex
word persp = 300 + rotatedz[i]/256
screenx[i] = rx / persp + width/2 as ubyte + 20
screeny[i] = ry / persp + height/2 as ubyte + 40
}
}
@@ -87,15 +93,15 @@ main {
for i1 in 0 to i {
ubyte i2 = i1+1
if(rotatedz[i1] > rotatedz[i2]) {
word @zp temp = rotatedx[i1]
rotatedx[i1] = rotatedx[i2]
rotatedx[i2] = temp
temp = rotatedy[i1]
rotatedy[i1] = rotatedy[i2]
rotatedy[i2] = temp
temp = rotatedz[i1]
word @zp tempz = rotatedz[i1]
rotatedz[i1] = rotatedz[i2]
rotatedz[i2] = temp
rotatedz[i2] = tempz
ubyte @zp tempx = screenx[i1]
screenx[i1] = screenx[i2]
screenx[i2] = tempx
ubyte @zp tempy = screeny[i1]
screeny[i1] = screeny[i2]
screeny[i2] = tempy
}
}
}
@@ -104,11 +110,8 @@ main {
for i in 0 to 7 {
word @zp zc = rotatedz[i]
word persp = 300+zc/256
ubyte sx = rotatedx[i] / persp + width/2 as ubyte + 20
ubyte sy = rotatedy[i] / persp + height/2 as ubyte + 40
c64.SPXYW[i] = mkword(sy, sx)
c64.SPXYW[i] = mkword(screeny[i], screenx[i])
; because we want speed, we don't use the dynamic c64.set_sprite_ptr() here
if(zc < 30*128)
+13 -16
View File
@@ -10,10 +10,11 @@ main {
word[] ycoor = [ -40, -40, 40, 40, -40, -40, 40, 40 ]
word[] zcoor = [ -40, 40, -40, 40, -40, 40, -40, 40 ]
; storage for rotated coordinates
word[len(xcoor)] rotatedx
word[len(ycoor)] rotatedy
; storage for rotated Z (needed for back/front classification)
word[len(zcoor)] rotatedz
; precomputed screen coordinates (perspective-corrected) for drawing
byte[len(xcoor)] screenx
byte[len(ycoor)] screeny
sub start() {
@@ -64,9 +65,14 @@ main {
ubyte @zp i
for i in 0 to len(xcoor)-1 {
; don't normalize by dividing by 128, instead keep some precision for perspective calc later
rotatedx[i] = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
rotatedy[i] = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
word rx = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
word ry = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
rotatedz[i] = Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i]
; perspective projection, done once per vertex
word persp = 900 + (rotatedz[i]>>5)
screenx[i] = rx / persp as byte + txt.DEFAULT_WIDTH/2
screeny[i] = ry / persp as byte + txt.DEFAULT_HEIGHT/2
}
}
@@ -77,27 +83,18 @@ main {
ubyte @zp i
word @zp rz
word @zp persp
byte sx
byte sy
for i in 0 to len(xcoor)-1 {
rz = rotatedz[i]
if rz >= 10 {
persp = 900 + rz/32
sx = rotatedx[i] / persp as byte + txt.DEFAULT_WIDTH/2
sy = rotatedy[i] / persp as byte + txt.DEFAULT_HEIGHT/2
txt.setcc(sx as ubyte, sy as ubyte, 46, 7)
txt.setcc(screenx[i] as ubyte, screeny[i] as ubyte, 46, 7)
}
}
for i in 0 to len(xcoor)-1 {
rz = rotatedz[i]
if rz < 10 {
persp = 900 + rz/32
sx = rotatedx[i] / persp as byte + txt.DEFAULT_WIDTH/2
sy = rotatedy[i] / persp as byte + txt.DEFAULT_HEIGHT/2
txt.setcc(sx as ubyte, sy as ubyte, 81, 7)
txt.setcc(screenx[i] as ubyte, screeny[i] as ubyte, 81, 7)
}
}
}
+13 -16
View File
@@ -11,10 +11,11 @@ main {
float[] ycoor = [ -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0 ]
float[] zcoor = [ -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0 ]
; storage for rotated coordinates
float[len(xcoor)] rotatedx
float[len(ycoor)] rotatedy
; storage for rotated Z (needed for back/front classification)
float[len(zcoor)] rotatedz
; precomputed screen coordinates (perspective-corrected) for drawing
ubyte[len(xcoor)] screenx
ubyte[len(ycoor)] screeny
sub start() {
float time=0.0
@@ -64,9 +65,14 @@ main {
ubyte @zp i
for i in 0 to len(xcoor)-1 {
rotatedx[i] = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
rotatedy[i] = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
float rx = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
float ry = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
rotatedz[i] = Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i]
; perspective projection, done once per vertex
float persp = (5.0+rotatedz[i])/(txt.DEFAULT_HEIGHT as float)
screenx[i] = rx / persp + txt.DEFAULT_WIDTH/2.0 as ubyte
screeny[i] = ry / persp + txt.DEFAULT_HEIGHT/2.0 as ubyte
}
}
@@ -76,27 +82,18 @@ main {
; first the points on the back, then the points on the front (painter algorithm)
ubyte @zp i
float rz
float persp
ubyte sx
ubyte sy
for i in 0 to len(xcoor)-1 {
rz = rotatedz[i]
if rz >= 0.1 {
persp = (5.0+rz)/(txt.DEFAULT_HEIGHT as float)
sx = rotatedx[i] / persp + txt.DEFAULT_WIDTH/2.0 as ubyte
sy = rotatedy[i] / persp + txt.DEFAULT_HEIGHT/2.0 as ubyte
txt.setcc(sx, sy, 46, 1)
txt.setcc(screenx[i], screeny[i], 46, 1)
}
}
for i in 0 to len(xcoor)-1 {
rz = rotatedz[i]
if rz < 0.1 {
persp = (5.0+rz)/(txt.DEFAULT_HEIGHT as float)
sx = rotatedx[i] / persp + txt.DEFAULT_WIDTH/2.0 as ubyte
sy = rotatedy[i] / persp + txt.DEFAULT_HEIGHT/2.0 as ubyte
txt.setcc(sx, sy, 81, 1)
txt.setcc(screenx[i], screeny[i], 81, 1)
}
}
}
+12 -13
View File
@@ -11,10 +11,9 @@ main {
word[] ycoor = [ -100, -100, 100, 100, -100, -100, 100, 100 ]
word[] zcoor = [ -100, 100, -100, 100, -100, 100, -100, 100 ]
; storage for rotated coordinates
word[len(xcoor)] rotatedx
word[len(ycoor)] rotatedy
word[len(zcoor)] rotatedz
; storage for screen coordinates (perspective-corrected)
word[len(xcoor)] screenx
word[len(ycoor)] screeny
; edges
ubyte[] edgesFrom = [ 0, 2, 6, 4, 1, 3, 7, 5, 0, 2, 6, 4]
@@ -69,9 +68,14 @@ main {
ubyte @zp i
for i in 0 to len(xcoor)-1 {
; don't normalize by dividing by 128, instead keep some precision for perspective calc later
rotatedx[i] = (Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i])
rotatedy[i] = (Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i])
rotatedz[i] = (Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i])
word rx = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
word ry = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
word rz = Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i]
; perspective projection, done once per vertex
word persp = 256 + (rz>>8)
screenx[i] = rx / persp + graphics.WIDTH/2 as uword
screeny[i] = ry / persp + graphics.HEIGHT/2 as ubyte
}
}
@@ -80,12 +84,7 @@ main {
for i in len(edgesFrom) -1 downto 0 {
ubyte @zp vFrom = edgesFrom[i]
ubyte @zp vTo = edgesTo[i]
word @zp persp1 = 256 + rotatedz[vFrom]/256
word @zp persp2 = 256 + rotatedz[vTo]/256
graphics.line(rotatedx[vFrom] / persp1 + graphics.WIDTH/2 as uword,
rotatedy[vFrom] / persp1 + graphics.HEIGHT/2 as ubyte,
rotatedx[vTo] / persp2 + graphics.WIDTH/2 as uword,
rotatedy[vTo] / persp2 + graphics.HEIGHT/2 as ubyte)
graphics.line(screenx[vFrom] as uword, screeny[vFrom] as ubyte, screenx[vTo] as uword, screeny[vTo] as ubyte)
}
}
}
+59 -79
View File
@@ -83,76 +83,38 @@ main {
for i in shipdata.totalNumberOfEdges -1 downto 0 {
ubyte @zp vFrom = shipdata.edgesFrom[i]
ubyte @zp vTo = shipdata.edgesTo[i]
word persp1 = 200 + matrix_math.rotatedz[vFrom]/256
word persp2 = 200 + matrix_math.rotatedz[vTo]/256
monogfx.line(matrix_math.rotatedx[vFrom] / persp1 + screen_width/2 as uword,
matrix_math.rotatedy[vFrom] / persp1 + screen_height/2 as uword,
matrix_math.rotatedx[vTo] / persp2 + screen_width/2 as uword,
matrix_math.rotatedy[vTo] / persp2 + screen_height/2 as uword,
monogfx.line(matrix_math.screenx[vFrom] as uword,
matrix_math.screeny[vFrom] as uword,
matrix_math.screenx[vTo] as uword,
matrix_math.screeny[vTo] as uword,
true)
}
}
sub draw_lines_hiddenremoval() {
; complex drawing routine that draws the ship model based on its faces,
; where it uses the surface normals to determine visibility.
sys.memset(edgestodraw, shipdata.totalNumberOfEdges, 1)
ubyte @zp edgeIdx = 0
ubyte @zp pointIdx = 0
; determine visibility of each face
ubyte faceNumber
for faceNumber in shipdata.totalNumberOfFaces -1 downto 0 {
if matrix_math.facing_away_fast_but_imprecise(pointIdx) { ;; can also use matrix_math.facing_away_slow_but_precise(pointIdx)
; don't draw this face, fast-forward over the edges and points
edgeIdx += 3 ; every face hast at least 3 edges
while shipdata.facesEdges[edgeIdx]!=255 {
edgeIdx++
}
edgeIdx++
pointIdx += 3 ; every face has at least 3 points
while shipdata.facesPoints[pointIdx]!=255 {
pointIdx++
}
pointIdx++
} else {
; draw this face
ubyte @zp e1 = shipdata.facesEdges[edgeIdx]
edgeIdx ++
ubyte @zp e2 = shipdata.facesEdges[edgeIdx]
edgeIdx ++
ubyte @zp e3 = shipdata.facesEdges[edgeIdx]
edgeIdx ++
if edgestodraw[e1]
draw_edge(e1)
if edgestodraw[e2]
draw_edge(e2)
while e3!=255 {
if edgestodraw[e3]
draw_edge(e3)
e3 = shipdata.facesEdges[edgeIdx]
edgeIdx ++
}
; skip the rest of the facesPoints, we don't need them here anymore
pointIdx += 3 ; every face has at least 3 points
while shipdata.facesPoints[pointIdx]!=255 {
pointIdx++
}
pointIdx++
for faceNumber in 0 to matrix_math.FACE_COUNT-1 {
matrix_math.face_visible[faceNumber] = not matrix_math.facing_away_fast_but_imprecise(matrix_math.facePointIdx[faceNumber])
}
; draw every edge that belongs to at least one visible face
ubyte @zp edgeIdx
for edgeIdx in 0 to shipdata.totalNumberOfEdges-1 {
if matrix_math.face_visible[matrix_math.edgeFaceA[edgeIdx]] or
(matrix_math.edgeFaceB[edgeIdx] != 255 and matrix_math.face_visible[matrix_math.edgeFaceB[edgeIdx]]) {
draw_edge(edgeIdx)
}
}
}
bool[shipdata.totalNumberOfEdges] edgestodraw
sub draw_edge(ubyte edgeidx) {
edgestodraw[edgeidx] = false
ubyte vFrom = shipdata.edgesFrom[edgeidx]
ubyte vTo = shipdata.edgesTo[edgeidx]
word persp1 = 170 + matrix_math.rotatedz[vFrom]/256
word persp2 = 170 + matrix_math.rotatedz[vTo]/256
monogfx.line(matrix_math.rotatedx[vFrom] / persp1 + screen_width/2 as uword,
matrix_math.rotatedy[vFrom] / persp1 + screen_height/2 as uword,
matrix_math.rotatedx[vTo] / persp2 + screen_width/2 as uword,
matrix_math.rotatedy[vTo] / persp2 + screen_height/2 as uword,
monogfx.line(matrix_math.screenx[vFrom] as uword,
matrix_math.screeny[vFrom] as uword,
matrix_math.screenx[vTo] as uword,
matrix_math.screeny[vTo] as uword,
true)
}
}
@@ -164,6 +126,17 @@ matrix_math {
word[shipdata.totalNumberOfPoints] rotatedx
word[shipdata.totalNumberOfPoints] rotatedy
word[shipdata.totalNumberOfPoints] rotatedz
; precomputed screen coordinates (perspective-corrected) for drawing
word[shipdata.totalNumberOfPoints] screenx
word[shipdata.totalNumberOfPoints] screeny
const ubyte FACE_COUNT = 22
; start index of each face in shipdata.facesPoints
ubyte[FACE_COUNT] facePointIdx = [ 0, 4, 9, 13, 17, 22, 26, 30, 35, 39, 43, 47, 51, 59, 64, 69, 73, 77, 82, 87, 92, 97 ]
; for each edge, the one or two incident faces (255 means no second face)
ubyte[shipdata.totalNumberOfEdges] edgeFaceA = [ 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 10, 11, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 19, 19, 19, 20, 20, 21, 21 ]
ubyte[shipdata.totalNumberOfEdges] edgeFaceB = [ 1, 5, 2, 12, 7, 4, 9, 3, 11, 4, 11, 12, 8, 6, 7, 10, 12, 10, 9, 12, 12, 12, 12, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 18, 255, 20, 255, 21, 19, 21, 255, 20, 255, 255, 255, 255 ]
bool[FACE_COUNT] face_visible
sub rotate_vertices(ubyte ax, ubyte ay, ubyte az) {
; rotate around origin (0,0,0)
@@ -176,18 +149,18 @@ matrix_math {
word wcosc = math.cos8(az)
word wsinc = math.sin8(az)
word wcosa_sinb = wcosa*wsinb / 128
word wsina_sinb = wsina*wsinb / 128
word wcosa_sinb = wcosa*wsinb >> 7
word wsina_sinb = wsina*wsinb >> 7
word Axx = wcosa*wcosb / 128
word Axy = (wcosa_sinb*wsinc - wsina*wcosc) / 128
word Axz = (wcosa_sinb*wcosc + wsina*wsinc) / 128
word Ayx = wsina*wcosb / 128
word Ayy = (wsina_sinb*wsinc + wcosa*wcosc) / 128
word Ayz = (wsina_sinb*wcosc - wcosa*wsinc) / 128
word Axx = wcosa*wcosb >> 7
word Axy = (wcosa_sinb*wsinc - wsina*wcosc) >> 7
word Axz = (wcosa_sinb*wcosc + wsina*wsinc) >> 7
word Ayx = wsina*wcosb >> 7
word Ayy = (wsina_sinb*wsinc + wcosa*wcosc) >> 7
word Ayz = (wsina_sinb*wcosc - wcosa*wsinc) >> 7
word Azx = -wsinb
word Azy = wcosb*wsinc / 128
word Azz = wcosb*wcosc / 128
word Azy = wcosb*wsinc >> 7
word Azz = wcosb*wcosc >> 7
ubyte @zp i
for i in 0 to shipdata.totalNumberOfPoints-1 {
@@ -195,6 +168,13 @@ matrix_math {
rotatedx[i] = Axx*shipdata.xcoor[i] + Axy*shipdata.ycoor[i] + Axz*shipdata.zcoor[i]
rotatedy[i] = Ayx*shipdata.xcoor[i] + Ayy*shipdata.ycoor[i] + Ayz*shipdata.zcoor[i]
rotatedz[i] = Azx*shipdata.xcoor[i] + Azy*shipdata.ycoor[i] + Azz*shipdata.zcoor[i]
; perspective projection, done once per vertex instead of once per edge
word persp = 170 + rotatedz[i]/256
if persp < 32
persp = 32
screenx[i] = rotatedx[i] / persp + 160 as uword
screeny[i] = rotatedy[i] / persp + 120 as uword
}
}
@@ -207,12 +187,12 @@ matrix_math {
edgePointsIdx++
ubyte p3 = shipdata.facesPoints[edgePointsIdx]
word p1x = rotatedx[p1] / 128
word p1y = rotatedy[p1] / 128
word p2x = rotatedx[p2] / 128
word p2y = rotatedy[p2] / 128
word p3x = rotatedx[p3] / 128
word p3y = rotatedy[p3] / 128
word p1x = rotatedx[p1] >> 7
word p1y = rotatedy[p1] >> 7
word p2x = rotatedx[p2] >> 7
word p2y = rotatedy[p2] >> 7
word p3x = rotatedx[p3] >> 7
word p3y = rotatedy[p3] >> 7
return (p2x-p3x)*(p1y-p3y) - (p2y-p3y)*(p1x-p3x) > 0
}
@@ -225,12 +205,12 @@ matrix_math {
ubyte p3 = shipdata.facesPoints[edgePointsIdx]
; Calculate two edge vectors of the triangle (scaled by 2)
word v1x = (rotatedx[p2] - rotatedx[p1])/128
word v1y = (rotatedy[p2] - rotatedy[p1])/128
word v1z = (rotatedz[p2] - rotatedz[p1])/128
word v2x = (rotatedx[p3] - rotatedx[p1])/128
word v2y = (rotatedy[p3] - rotatedy[p1])/128
word v2z = (rotatedz[p3] - rotatedz[p1])/128
word v1x = (rotatedx[p2] - rotatedx[p1]) >> 7
word v1y = (rotatedy[p2] - rotatedy[p1]) >> 7
word v1z = (rotatedz[p2] - rotatedz[p1]) >> 7
word v2x = (rotatedx[p3] - rotatedx[p1]) >> 7
word v2y = (rotatedy[p3] - rotatedy[p1]) >> 7
word v2z = (rotatedz[p3] - rotatedz[p1]) >> 7
; Calculate surface normal using cross product: N = V1 x V2 (scaled by 4)
; Note: because of lack of precision in the 16 bit word math, we need to use floating point math here.... :-(
+13 -16
View File
@@ -28,27 +28,18 @@ main {
ubyte @zp i
word @zp rz
word @zp persp
byte sx
byte sy
for i in 0 to len(matrix_math.xcoor)-1 {
rz = matrix_math.rotatedz[i]
if rz >= 10 {
persp = 400 + rz/64
sx = matrix_math.rotatedx[i] / persp as byte + txt.DEFAULT_WIDTH/2
sy = matrix_math.rotatedy[i] / persp as byte + txt.DEFAULT_HEIGHT/2
txt.setcc(sx as ubyte, sy as ubyte, 46, 7)
txt.setcc(matrix_math.screenx[i] as ubyte, matrix_math.screeny[i] as ubyte, 46, 7)
}
}
for i in 0 to len(matrix_math.xcoor)-1 {
rz = matrix_math.rotatedz[i]
if rz < 10 {
persp = 400 + rz/64
sx = matrix_math.rotatedx[i] / persp as byte + txt.DEFAULT_WIDTH/2
sy = matrix_math.rotatedy[i] / persp as byte + txt.DEFAULT_HEIGHT/2
txt.setcc(sx as ubyte, sy as ubyte, 81, 7)
txt.setcc(matrix_math.screenx[i] as ubyte, matrix_math.screeny[i] as ubyte, 81, 7)
}
}
}
@@ -62,10 +53,11 @@ matrix_math {
word[] ycoor = [ -40, -40, 40, 40, -40, -40, 40, 40 ]
word[] zcoor = [ -40, 40, -40, 40, -40, 40, -40, 40 ]
; storage for rotated coordinates
word[len(xcoor)] rotatedx
word[len(ycoor)] rotatedy
; storage for rotated Z (needed for back/front classification)
word[len(zcoor)] rotatedz
; precomputed screen coordinates (perspective-corrected) for drawing
byte[len(xcoor)] screenx
byte[len(ycoor)] screeny
sub rotate_vertices(ubyte ax, ubyte ay, ubyte az) {
; rotate around origin (0,0,0)
@@ -94,9 +86,14 @@ matrix_math {
ubyte @zp i
for i in 0 to len(xcoor)-1 {
; don't normalize by dividing by 128, instead keep some precision for perspective calc later
rotatedx[i] = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
rotatedy[i] = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
word rx = Axx*xcoor[i] + Axy*ycoor[i] + Axz*zcoor[i]
word ry = Ayx*xcoor[i] + Ayy*ycoor[i] + Ayz*zcoor[i]
rotatedz[i] = Azx*xcoor[i] + Azy*ycoor[i] + Azz*zcoor[i]
; perspective projection, done once per vertex
word persp = 400 + (rotatedz[i]>>6)
screenx[i] = rx / persp as byte + txt.DEFAULT_WIDTH/2
screeny[i] = ry / persp as byte + txt.DEFAULT_HEIGHT/2
}
}
-83
View File
@@ -1,83 +0,0 @@
%zeropage basicsafe
%option no_sysinit
main {
ubyte[8] scratch
ubyte counter
sub start() {
scratch[0] = 10
scratch[1] = 20
; Pattern 1: PtSub 2-byte-param spill
; both args are array-indexed reads (non-simple) -> needAsaveForExpr true for both
two_byte_sub(scratch[0], scratch[1])
; Pattern 2: Asmsub XY-A spill
; XY gets simple identifier, A gets value from a non-inlinable function call
asm_xy_a(scratch, get_value(counter))
; Pattern 3: Asmsub Y-X spill
; Y gets number, X gets binary expression with non-inlinable function call
asm_y_x(0, get_value(counter) + 5)
; Pattern 4: Asmsub Y-X-A spill
; Y gets number, X gets binary expression with non-inlinable function call
; (A gets simple number, no further spill)
asm_y_x_a(0, get_value(counter) + 3, 42)
; Pattern 5: Library asmsub Y-X (txt.plot) spill
; Y gets simple number, X gets complex expression -> Y saved before eval of X
txt.plot(0, get_value(counter) + 5)
; Pattern 6: Library asmsub X-Y-A (txt.setchr) spill
; Y (row) gets simple number, X (col) gets complex expression -> Y saved before eval of X
; (A gets simple number, no further spill)
txt.setchr(get_value(counter), 0, 42)
}
sub two_byte_sub(ubyte a, ubyte b) {
counter += a
counter += b
}
sub get_value(ubyte x) -> ubyte {
if x==0
return 42
return x
}
asmsub asm_xy_a(uword ptr @XY, ubyte val @A) clobbers(A) {
%asm {{
rts
}}
}
asmsub asm_y_x(ubyte col @Y, ubyte row @X) {
%asm {{
rts
}}
}
asmsub asm_y_x_a(ubyte col @Y, ubyte row @X, ubyte ch @A) clobbers(A) {
%asm {{
rts
}}
}
}
txt {
asmsub plot(ubyte col @ Y, ubyte row @ X) {
%asm {{
clc
jmp $FFF0
}}
}
asmsub setchr(ubyte col @X, ubyte row @Y, ubyte character @A) clobbers(A) {
%asm {{
rts
}}
}
}
+4 -18
View File
@@ -3,24 +3,10 @@
main {
sub start() {
; Issue 4: @(...) is always an UNSIGNED byte read. The optimizer rewrite
; @(&x) -> x (ConstantFoldingOptimizer / MemoryOptimizers) must not turn the
; unsigned memory read into the signed variable, which would sign-extend on widen.
; Reading -1 from memory (0xFF) must give 255, not 65535.
byte @shared sv = -1 ; memory holds 0xFF
word a = @(&sv) ; zero-extends the unsigned byte to 255
txt.print("mem-read signed byte -1: ")
txt.print_w(a)
txt.print(" (raw 0xFF; zero-extends to 255)\n")
; Contrast: an UNSIGNED byte read via @(&x) is correctly zero-extended.
ubyte @shared uv = 255 ; memory also holds 0xFF
word b = @(&uv) ; zero-extends to 255
txt.print("mem-read unsigned byte 255: ")
txt.print_w(b)
txt.print(" (raw 0xFF; zero-extends to 255)\n")
txt.print("done\n")
;sys.poweroff_system()
for cx16.r0L in 5 downto 0 {
txt.print_ub(cx16.r0L)
txt.spc()
}
}
}
+171
View File
@@ -0,0 +1,171 @@
# Pointer Dereference Codegen Analysis
## Bug
In the old 6502 codegen, a bare typed-pointer dereference such as:
```prog8
^^pointer temp = &table
while temp^^ != $0000 {
temp++
}
```
corrupted `temp` itself. The generated code loaded the value pointed to by `temp` and wrote it back into `temp`, so the next iteration read from the wrong address.
## Root Cause
`PointerAssignmentsGen.deref()` in `codeGenCpu6502` does not return a uniform abstraction. Depending on the length of the pointer-deref chain, it returns one of three different things:
| Chain length | Example | What `deref()` returns |
|--------------|---------|------------------------|
| 0 | `temp^^` | the pointer variable itself |
| 1 | `foo.bar^^` | address of the final pointer field |
| >1 | `foo.bar.baz^^` | address of the final value (the pointer stored in the final field is loaded into `P8ZP_SCRATCH_PTR`, offset 0) |
`assignPointerDerefExpression()` then had to know which case it received. The original code always performed an extra word load and wrote it back into the returned pointer variable:
```kotlin
asmgen.loadIndirectWordAY(zpPtrVar, offset1)
asmgen.out(" sta $zpPtrVar | sty $zpPtrVar+1")
```
This is safe when `zpPtrVar` is the temporary scratch pointer `P8ZP_SCRATCH_PTR` (field-access cases), but wrong when `zpPtrVar` is the user's own pointer variable (bare `temp^^`), because it overwrites the variable.
## Applied Fix
Changed the extra dereference in `assignPointerDerefExpression()` to run only for non-empty chains:
```kotlin
if(value.derefLast && value.chain.isNotEmpty()) {
asmgen.loadIndirectWordAY(zpPtrVar, offset1)
asmgen.out(" sta $zpPtrVar | sty $zpPtrVar+1")
offset = 0.toUByte()
} else {
offset = offset1
}
```
- Bare variable derefs (`temp^^`) now read directly from the variable without corrupting it.
- Struct-field pointer derefs (`foo.bar^^`, `foo.bar.baz^^`) still get the extra dereference they need, because `deref()` uses `P8ZP_SCRATCH_PTR` for those cases.
Tests added:
- `double pointer dereference in while loop (code gen fix)` — covers the bare-variable case.
- `pointer-to-pointer struct field deref still double-derefs on 6502` — preserves the existing double-deref behavior for struct fields.
## Comparison with IR-Based Codegens
The IR codegens (`codeGenIntermediate`, used by virtual, `new6502`, and m68k) handle this more cleanly. In `ExpressionGen.kt`:
1. `translate(startpointer)` evaluates the base pointer into a register.
2. `traverseRestOfDerefChainToCalculateFinalAddress()` walks the chain and always produces a register that points at the final value (or a field offset when there is no final dereference).
3. A single `LOADI` reads from `(pointerReg, offset)`.
The empty-chain case falls out naturally: the traverse function does nothing, so the load reads directly from the startpointer register. The caller never has to decode different abstraction levels based on chain length.
## Remaining Technical Debt
The precise inconsistency is narrower than the chain-length table suggests. For chains of length 1 and >1, `deref()` already returns the same abstraction: a pointer to the final value plus a Y offset. Only the empty-chain case differs: `deref()` may return the user's own zeropage pointer variable (when `isZpVar()`), and by construction any caller write-back into the returned variable is forbidden there, whereas in all other cases the returned variable is `P8ZP_SCRATCH_PTR`, which callers may safely write back into.
The current fix only prevents the write-back in `assignPointerDerefExpression()`. A proper cleanup would make `deref()` always return `P8ZP_SCRATCH_PTR` (copying the user's pointer there when the chain is empty), or alternatively make the contract explicit with a "may clobber the returned pointer" flag that callers must obey. Either form restores one uniform abstraction for all chain lengths and lets `assignPointerDerefExpression()` treat every case identically, matching the IR codegen structure in `ExpressionGen.kt`. The trade-off of always copying to the scratch pointer is one extra 2-byte copy for the common bare-variable case.
## Verified Semantic Fact (empirical)
Test program on 2026-10-19 (both targets compiled with the current compiler build):
```prog8
struct Inner { ^^uword p }
struct Outer { ^^Inner inner }
^Outer o; ^Inner ip
o.inner = ip ; ip.p = $1000 ; pokew($1000, $aaaa) ; pokew($aaaa, $1234)
result = o.inner.p^^
```
- `virtual` target: prints `aaaa`.
- `c64` target: generated assembly computes `AY = word at (scratch)` (= value of field `p`, `$1000`) followed by `jsr prog8_lib.func_peekw`, so `result = *(p content)` = `aaaa` as well.
So both backends agree on the invariant: **for `derefLast` chains the value is `*(pointer stored in the final field)`; each pointer in the chain is followed exactly once, plus one read of the pointed-to word.** This validates the fix's premise (double deref for `foo.bar^^`, via implicit follow of `o` plus follow of the stored pointer). Note also that `CodeDesugarer` rewrites many scalar-field deref chains into `peekw/peekl` calls before codegen (see `CodeDesugarer.kt:1569-1622`), so `deref()` only ever receives the shapes that survive desugaring; the exact inventory of surviving shapes is part of phase 0 below.
## Proposed Plan: normalize `deref()` to a single contract
### Target contract (mirrors the IR codegen)
`deref()` always returns `(zpVar, offset)` such that **one indirect access through `zpVar` at byte `offset` reaches the final thing**:
- `derefLast=true`: `(zpVar),offset` is the address of the value itself. One word read yields the value; one memory write with `(zpVar),offset` stores it.
- `derefLast=false`: `(zpVar),offset` is the location of the field content (typically a pointer field); reads and writes go directly through it.
- `addOffsetToPointer=true` and `loadPointerFieldValue=true`: unchanged; this mode is already "materialize the effective address" and every current subbranch already consumes all follows it needs (`PointerAssignmentsGen.kt:279-304`, follows for `derefLast`).
Key point: this is precisely the IR codegen's contract (`traverseRestOfDerefChainToCalculateFinalAddress` returns a reg+offset pair) so that the same single `LOADI`-equivalent works everywhere.
### Current conformance gaps (all in `deref()`, only for `derefLast`)
| shape | follows inside `deref()` | conforms |
|-------|--------------------------|----------|
| chain 0 (`temp^^`) | 0 (`deref()` returns the variable itself) | yes (`(temp),off` already reaches the value) |
| chain 1, `addOffsetToPointer=true` | final field followed (`deref:282-284`) | yes |
| chain 1, `addOffsetToPointer=false` | final field NOT followed (`deref:286-304`); caller must follow | **no** |
| chain >1 | every field followed, including the final one (`deref:312-328`) | yes |
| chain 1/`>1`, `derefLast=false` | field content location returned with offset | yes |
So chain>1 vs chain<=1 asymmetry is the *only* structural leak: the fixed `assignPointerDerefExpression()` has to decide "was the final follow consumed?" by testing `chain.isNotEmpty()`.
### Phase 1 - move the final follow into deref() (mechanical)
In the `chain.size==1 && !addOffsetToPointer && derefLast` subbranches (`PointerAssignmentsGen.kt:286-304`, both the `isZpVar` and the copy-to-scratch path), call `updateScratchPointer()` right after `addFieldOffsetToScratchPointer()` and return `(P8ZP_SCRATCH_PTR, 0)`. After phase 2 removes the now-duplicated caller-side follow, the follow is neither duplicated nor dropped.
Phase 1 is NOT size-neutral or behavior-neutral: only `assignPointerDerefExpression()` has a matching follow to delete. Every other caller of `deref()` with default args receives a followed pointer plus one extra `updateScratchPointer` (~8 bytes) for chain==1 `derefLast`, with nothing to remove on their side. The affected call sites are the assign* helpers (`PointerAssignmentsGen.kt:37-230`, writes and inplace negate/invert), `inplaceModificationPointerField` (`AugmentableAssignmentAsmGen.kt:134`), the const-index pointer-array read (`AssignmentAsmGen.kt:262`), the address-of read paths (`AssignmentAsmGen.kt:563,749`, `PointerAssignmentsGen.kt:945`), and the `AugmentableAssignmentAsmGen` RMW dispatch. For write targets (`foo.bar^^ = v`) this also changes semantics: today those helpers store into the field location (`&foo.bar`), after phase 1 they store through the followed pointer (`*(foo.bar)`), which matches the IR backend. This is an intended behavior change, not a relocation; size deltas of a few bytes UP per affected site are expected and must be attributed per the documented rule.
### Phase 2 - delete the caller's private final follow (mechanical, small)
Only one call site has a compensating follow to delete: `assignPointerDerefExpression()` (`PointerAssignmentsGen.kt:397-408`) is the only place in the codebase with the `loadIndirectWordAY(zpPtrVar, offset1)` + `sta $zpPtrVar | sty $zpPtrVar+1` write-back pattern (verified by grep on 2026-10-19). Delete the entire `if(value.derefLast && value.chain.isNotEmpty())` block there; the bare-read path that was the "fix" stays as the only path. Do not hunt for further follows to remove: every other `loadIndirectWordAY(zpPtrVar, 0u)` site is either behind `addOffsetToPointer=true` (a branch phase 1 does not touch) or is an RMW load of the current value being multiplied, divided, or reduced (`PointerAssignmentsGen.kt:1941-2104`), which must be kept.
Also required: a comment update to `derefUsesScratchPointer()` to match the new contract; no behavioral change there (which paths use `P8ZP_SCRATCH_PTR` does not change - we only added one follow instruction inside already-scratch paths).
### Phase 0 (before phase 1) - instrument the surviving shapes
`CodeDesugarer` already converts a large class of scalar-field deref chains into `peek/peekw/peekl` calls, so `deref()` is only exercised for a subset of shapes. Build a dev-only instrumented `deref()` that logs `(chain.size, derefLast, addOffsetToPointer, loadPointerFieldValue, isZpVar)` tuples across `prog8c` on the whole `examples/` tree and the compiler's own test corpus, plus all new tests. Any tuple that does not match the conformance table above is discovered before it can break a caller-cleanup edit.
### Phase 3 - verification
- Extend `TestPointers.kt` with tests for the branch phase 1 actually changes - chain==1 `derefLast`: a read (`foo.bar^^`, asserting the double deref result, as a `simulate()`-based test) and a write (`foo.bar^^ = v`, asserting the store lands at `*(foo.bar)`, not in the field). The existing test `pointer-to-pointer struct field deref still double-derefs on 6502` is the regression anchor for the read side and must stay green.
- Note what does NOT cover the change: the discriminating `$aaaa`/`$1234` chain>1 read program from the section above is desugared into `peekw(...)` before the 6502 codegen (detriment #1), so a `simulate()` test of it exercises the desugarer+peekw path and passes regardless of phase 1/2 correctness. A chain>1 write (`x.inner.p^^ = v`) exercises the chain>1 branch, which phase 1 does not modify. Both are fine as additional coverage but cannot substitute for the chain==1 tests.
- `prog8tests.compiler.TestPointers` green; then `TestCompilerOnExamples` expected sizes, following the documented rules: diff the generated `.asm` of shifted examples before updating each expected size (phase 1 shifts sizes UP slightly at chain==1-`derefLast` sites - see phase 1; phase 2 shifts sizes down slightly).
- Final guards: `-noopt` baseline runs are already covered by existing tests; no new failure modes expected because no optimizer participates in this change.
### Alternative rejected: explicit three-way contract
A cheaper fix would change the return type to a small data class `(zpVar, offset, finalFollowConsumed: Boolean)` so each caller branches on data instead of guessing by chain length. Rejected as the main plan because it makes the leak permanent: every current and future caller must still branch, and the internal-only inconsistency survives. It remains the right fallback if phase-2 caller cleanup turns out to be riskier than expected.
### Potential detriments: cases the implementer must verify
The normalization itself touches only the chain==1-`derefLast`, `addOffsetToPointer=false` subbranches of `deref()` (chain 0, chain>1, and all `addOffsetToPointer=true` paths are untouched), but the following cases can be silently damaged by an over-eager phase-2 cleanup. Each lists the today-evidence and what to verify before and after the edit (compile with and without `-noopt`, diff `.asm`):
1. **Chain>1 value reads have already been desugared away - do not delete their "follows".**
Verified on 2026-10-19: `result = o.inner.p^^` reaches the codegen as `peekw(<derefLast=false chain arg>)` (desugarer rewrite); the emitted asm shows exactly one `updateScratchPointer` (the chain>1 `derefLast` branch is bypassed) plus `jsr prog8_lib.func_peekw`. The `loadIndirectWordAY(zpPtrVar, 0u)`-shaped site that loads the field pointer for the peek argument is a **field read**, not a leftover follow. Phase 2's "delete the self-follow" rule applies **only** to the `loadIndirectWordAY(...)` + `sta $zpPtrVar | sty $zpPtrVar+1` write-back pattern behind `derefLast=true`. Deleting a plain derefLast=false value load corrupts the peek argument.
2. **Address-of-derefLast shapes change meaning under phase 1.**
`PtAddressOf` on a chain (`AssignmentAsmGen.kt:740-764`, `PointerAssignmentsGen.readByteByAddressOfDereference:943-953`): for chain 0, today's behavior (copy the pointer variable's content = the value address) is correct and unchanged. For chain 1 with `derefLast`, today's callers consume the returned var as "location holding the pointer to be dereferenced"; after phase 1 `deref()` returns the followed pointer instead, shifting such reads/writes by one indirection level. Verify whether any `&(chain-1-derefLast)` shape survives `CodeDesugarer` at all; if yes, align to the IR backend's semantics (address-of-value = the followed pointer) and add `simulate()` regression tests for it. (The swap paths in `AsmGen.kt` are NOT affected: the non-optimized swap calls `deref(v, true)` with `addOffsetToPointer=true`, and the zeropage fast path at `AsmGen.kt:1550` bypasses `deref()` entirely.)
3. **RMW / augmented-assignment paths must keep using the same scratch layout.**
`AugmentableAssignmentAsmGen.inplaceModificationPointerField` has a hard `require(zpPtrVar=="P8ZP_SCRATCH_PTR")` and relies on `derefUsesScratchPointer()` to decide between in-place-through-variable vs the preserve-address-on-stack scheme. Phase 1 must not flip any of those decisions (chain-0 and chain-1-`!derefLast` zp fast paths stay zp-var-based). Verify with before/after asm diffs on `temp^^ += 1`, `foo.bar^^ += 1`, and bare derefLast targets; any flip from user-var to scratch (or back) changes the clobber-preservation assumptions and needs paired edits.
4. **`derefUsesScratchPointer()` re-derivation.**
No behavior change is expected (it keys on which paths use the scratch pointer, and phase 1 keeps exactly those paths scratch-bound), but grep callers for other places that branch on "deref returned a user variable for chain==1" (`isZpVar` checks inside callers) and update comments/conditions where the contract terminology changed.
5. **Sizes.**
Phase 1 shifts sizes UP slightly (one extra `updateScratchPointer` per chain==1-`derefLast` default-arg call site; see phase 1), phase 2 shifts sizes down slightly (the single deleted follow in `assignPointerDerefExpression()`). `TestCompilerOnExamples` expected sizes may also shift if the desugarer/CPU-optimizer interacts with restructured patterns. Follow the documented rule: revert + re-diff the generated `.asm` for each shifted example before bumping an expected size.
### Explicitly out of scope: existing frontend pointer-chain issues
Several pointer-chain compilation problems exist independently of `deref()` and belong to the deferred `todo.rst` item ("really fixing the pointer dereferencing issues - cursed hybrid between IdentifierReference, PtrDereference and PtrIndexedDereference"). Verified on 2026-10-19 on the current build: `while o.inner.p^^ != $aaaa { ... }` crashes the compiler with `FatalAstException: binexpr with '.' operator should have been converted into PtrDereference or something else` (AstChecker.kt:2603) - distinct frontend bug, ditto for `-noopt`. The `deref()` normalization work must NOT attempt to fix these.
Practical rules for the implementer:
- Phase 0's shape inventory must be recorded *per usage context* (assignment RHS, if/while condition, call argument, augmented assignment, swap, address-of), because `CodeDesugarer` produces contextually different shapes and some contexts don't compile at all today.
- Frontend crashes found during phase 0 are recorded as known-broken input values for the inventory, and are not fixed here.
- Regression policy: if a program fails after the refactor, first reproduce on the pre-refactor build (revert, rebuild). Only failures that appear with the refactor but not without it are in scope for this change; everything else is a separate bug report against the deferred pointer-hybrid item.
- The alternative "always return `P8ZP_SCRATCH_PTR` including chain 0" variant listed under Remaining Technical Debt IS detrimental (extra 2-byte copy + follow on every bare-deref read, the hottest shape - e.g. the while-loop scan that motivated the original fix); the follow-relocation plan above deliberately avoids that cost. If that variant is ever revisited, re-measure the bare-deref loops first.
See `docs/source/todo.rst` for the tracking item.
+1 -1
View File
@@ -4,4 +4,4 @@ org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.configuration-cache=false
kotlin.code.style=official
version=12.3
version=12.3.5-SNAPSHOT
@@ -38,9 +38,9 @@ private object IRStSymbolicReferenceXml {
is IRStSymbolicReference.BoolValue -> throw InternalCompilerException("bool in word array")
}
fun formatForStructField(ref: IRStSymbolicReference): String = when(ref) {
fun formatForStructField(ref: IRStSymbolicReference, dt: DataType): String = when(ref) {
is IRStSymbolicReference.BoolValue -> if(ref.value) "1" else "0"
is IRStSymbolicReference.Numeric -> ref.value.toInt().toHex()
is IRStSymbolicReference.Numeric -> if(dt.isFloat) ref.value.toString() else ref.value.toInt().toHex()
is IRStSymbolicReference.Symbol -> "@${ref.name}"
}
}
@@ -458,7 +458,7 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
listOf(field.type)
}
val values = expandedTypes.zip(instance.values).map {(dt, value) ->
val valuestr = IRStSymbolicReferenceXml.formatForStructField(value.value)
val valuestr = IRStSymbolicReferenceXml.formatForStructField(value.value, dt)
dt to valuestr
}
append(values.joinToString(",") { "${it.first}:${it.second}" })
@@ -1116,6 +1116,8 @@ data class IRInstruction(
return IRDataType.BYTE
if(opcode==Opcode.SQRT)
return IRDataType.WORD
if(opcode==Opcode.STOREZX)
return IRDataType.BYTE
}
if(opcode in setOf(Opcode.JUMPI, Opcode.CALLI, Opcode.STOREZI, Opcode.LSIGW, Opcode.MSIGW))
return IRDataType.WORD
+1 -1
View File
@@ -17,7 +17,7 @@ fun DataType.irTypeString(length: UInt?): String {
BaseDataType.WORD -> "word"
BaseDataType.LONG -> "long"
BaseDataType.FLOAT -> "float"
BaseDataType.STR -> "ubyte[$lengthStr]" // here string doesn't exist as a seperate datatype anymore
BaseDataType.STR -> "ubyte[$lengthStr]" // here string doesn't exist as a separate datatype anymore
BaseDataType.POINTER -> "uword"
BaseDataType.ARRAY_POINTER -> "uword"
BaseDataType.STRUCT_INSTANCE -> {
+49
View File
@@ -1,7 +1,9 @@
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.ints.shouldBeGreaterThan
import io.kotest.matchers.shouldBe
import prog8.code.core.BaseDataType
import prog8.code.core.CompilationOptions
import prog8.code.core.DataType
import prog8.code.core.OutputType
import prog8.code.core.ZeropageType
import prog8.code.target.Cx16Target
@@ -188,6 +190,53 @@ load.b r1,#0
struct.structName shouldBe "re.State"
}
test("test IR writer preserves decimal precision in float struct fields") {
// regression test: struct field floats used to be serialized via .toInt().toHex()
// which truncated the decimal portion (e.g. 1463.87 became 1463.0 in the loaded VM memory)
val target = Cx16Target()
val tempdir = Path(System.getProperty("java.io.tmpdir"))
val options = CompilationOptions.builder(target)
.output(OutputType.RAW)
.zeropage(ZeropageType.DONTUSE)
.noSysInit(true)
.compilerVersion("99.99")
.loadAddress(target.PROGRAM_LOAD_ADDRESS)
.memtopAddress(0xffffu)
.outputDir(tempdir)
.build()
val program = IRProgram("unittest-float-struct", IRSymbolTable(), options, target)
val structName = "main.Country"
val fields = listOf(
IRStStructField(DataType.forDt(BaseDataType.UWORD), "name"),
IRStStructField(DataType.forDt(BaseDataType.FLOAT), "population"),
IRStStructField(DataType.forDt(BaseDataType.UWORD), "area")
)
program.st.add(IRStStructDef(structName, fields, 8u))
val instanceName = "main.india_instance"
val population = 1463.87
val area = 3287
program.st.add(IRStStructInstance(
instanceName,
structName,
listOf(
IRStructInitValue(BaseDataType.UWORD, IRStSymbolicReference.Numeric(0.0)),
IRStructInitValue(BaseDataType.FLOAT, IRStSymbolicReference.Numeric(population)),
IRStructInitValue(BaseDataType.UWORD, IRStSymbolicReference.Numeric(area.toDouble()))
),
8u
))
val writer = IRFileWriter(program, null)
val generatedFile = writer.write()
val program2 = IRFileReader().read(generatedFile)
generatedFile.deleteExisting()
val instance = program2.st.allStructInstances().single { it.name == instanceName }
instance.values.size shouldBe 3
val floatValue = instance.values[1].value as IRStSymbolicReference.Numeric
floatValue.value shouldBe population
val intValue = instance.values[2].value as IRStSymbolicReference.Numeric
intValue.value shouldBe area.toDouble()
}
test("test IR reader parses loadhr/storehr sN immediate encoding") {
val source="""<?xml version="1.0" encoding="utf-8"?>
<PROGRAM NAME="test-sn-immediate" COMPILERVERSION="99.99">
+18 -8
View File
@@ -156,26 +156,36 @@ sealed class PtExpression(val type: DataType, position: Position) : PtNode(posit
}
}
fun isSimple(): Boolean {
// An expression is "simple" if evaluating it has no side effects and requires no temporary
// scratch storage or register preservation by the caller.
// The optional zeroPageVarCheck predicate (when given) additionally allows pointer derefs that can be
// evaluated without any codegen temporaries: the start pointer must already be a zeropage variable,
// so that the value can be read with a simple indirect indexed load from the pointer variable itself.
fun isSimple(zeroPageVarCheck: ((String) -> Boolean)? = null): Boolean {
return when(this) {
is PtAddressOf -> this.arrayIndexExpr==null || this.arrayIndexExpr?.isSimple()==true
is PtAddressOf -> this.arrayIndexExpr==null || this.arrayIndexExpr?.isSimple(zeroPageVarCheck)==true
is PtArray -> true
is PtArrayIndexer -> index is PtNumber || index is PtIdentifier
is PtBinaryExpression -> false
is PtContainmentCheck -> false
is PtFunctionCall -> if (this.builtin && name in SimpleBuiltinFunctions) args.all { it.isSimple() } else false
is PtFunctionCall -> if (this.builtin && name in SimpleBuiltinFunctions) args.all { it.isSimple(zeroPageVarCheck) } else false
is PtIdentifier -> true
is PtIrRegister -> true
is PtMemoryByte -> address is PtNumber
is PtBool -> true
is PtNumber -> true
is PtPrefix -> value.isSimple()
is PtPrefix -> value.isSimple(zeroPageVarCheck)
is PtRange -> true
is PtString -> true
is PtPointerDeref -> false
is PtTypeCast -> value.isSimple()
is PtIfExpression -> condition.isSimple() && truevalue.isSimple() && falsevalue.isSimple()
is PtBranchCondExpression -> truevalue.isSimple() && falsevalue.isSimple()
is PtPointerDeref ->
if(zeroPageVarCheck==null || type.isFloat || !zeroPageVarCheck(startpointer.name))
false
else
chain.isEmpty() // *pp : lda (pp),y (chain is never empty with derefLast=false)
|| (chain.size==1 && !derefLast) // pp.field : lda (pp+offset),y
is PtTypeCast -> value.isSimple(zeroPageVarCheck)
is PtIfExpression -> condition.isSimple(zeroPageVarCheck) && truevalue.isSimple(zeroPageVarCheck) && falsevalue.isSimple(zeroPageVarCheck)
is PtBranchCondExpression -> truevalue.isSimple(zeroPageVarCheck) && falsevalue.isSimple(zeroPageVarCheck)
is PtConstant -> true
}
}