mirror of
https://github.com/irmen/prog8.git
synced 2026-10-23 12:02:29 +00:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97dd081b02 | ||
|
|
2d3dc9d2d8 | ||
|
|
d183ba9f8f | ||
|
|
ef39144d26 | ||
|
|
0aca77e234 | ||
|
|
96a804ce3e | ||
|
|
b71787a929 | ||
|
|
ab8d924569 | ||
|
|
eafd8c62d2 | ||
|
|
ee09d35fdc | ||
|
|
af14562e2b | ||
|
|
ccc26a0eb2 | ||
|
|
e017c541bb | ||
|
|
3d5a7795d0 | ||
|
|
af06fdfffc | ||
|
|
e811ecdbde |
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) {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user