Compare 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
34 changed files with 1318 additions and 207 deletions

No files matched your search

@@ -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 {
@@ -1498,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)
@@ -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}")
@@ -2426,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)
@@ -2496,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 {
@@ -2540,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")
@@ -2554,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 -> {
@@ -4723,7 +4723,7 @@ $endLabel""")
pointergen.assignLongReg(IndexedPtrTarget(target), pairedRegisters)
return
}
asmgen.loadScaledArrayIndexIntoRegister(target.array!!, CpuRegister.Y)
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
@@ -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!!
@@ -228,10 +228,19 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
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)
@@ -276,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 {
@@ -284,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
@@ -294,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
@@ -326,6 +337,8 @@ 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
@@ -357,7 +370,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
// 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)
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("""
@@ -378,15 +391,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
}
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
}
val (zpPtrVar, offset) = deref(value)
if(value.type.isByteOrBool) {
asmgen.loadIndirectByte(zpPtrVar, offset)
asmgen.assignRegister(RegisterOrPair.A, target)
@@ -450,7 +455,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
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}")
@@ -462,7 +467,7 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
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}")
@@ -253,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)
@@ -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 {
@@ -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)
@@ -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)
@@ -2142,7 +2142,7 @@ internal class AstChecker(private val program: Program,
val deref = arrayIndexedExpression.pointerderef!!
val target = deref.definingScope.lookup(deref.chain)
val arraysize: Int? = when(target) {
is StructFieldRef -> target.field.arraySize
is StructFieldRef -> target.field.constSize()
is VarDecl -> target.arraysize?.constIndex()
else -> null
}
@@ -2342,10 +2342,17 @@ internal class AstChecker(private val program: Program,
|| dt.isPointer
|| dt.isArray
}
struct.fields.forEach { field ->
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)
}
}
}
@@ -2747,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) {
@@ -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)
}
@@ -420,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
@@ -463,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) {
@@ -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 {
+6 -6
View File
@@ -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)
)
+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{
+98
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
@@ -695,6 +696,73 @@ main {
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 {
@@ -745,4 +813,34 @@ main {
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
}
}
}
})
+55
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 {
+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
@@ -484,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
}
})
+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
}
@@ -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
}
}
+3 -2
View File
@@ -128,8 +128,9 @@ The ``alias`` statement makes it easier to refer to symbols from other places, a
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.
+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] {....}``
+7 -17
View File
@@ -1,22 +1,12 @@
%import textio
%zeropage basicsafe
main {
struct Thing {
uword port
^^Thing next
bool flag
}
sub start() {
^^Thing @shared t
; this compiles fine:
bool @shared derp1 = t.next.flag
ubyte @shared derp2 = t.next.flag as ubyte
bool @shared derp3 = (t.next).flag
ubyte @shared derp4 = (t.next).flag as ubyte
bool @shared derp5 = (t.port as ^^Thing).flag
ubyte @shared derp6 = (t.port as ^^Thing).flag as ubyte
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.3
version=12.3.5-SNAPSHOT