mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 03:02:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee09d35fdc | ||
|
|
af14562e2b | ||
|
|
ccc26a0eb2 | ||
|
|
e017c541bb | ||
|
|
3d5a7795d0 | ||
|
|
af06fdfffc | ||
|
|
e811ecdbde | ||
|
|
30435b6778 | ||
|
|
1358a535e3 | ||
|
|
c8f309307f | ||
|
|
b702ea50da | ||
|
|
05ba1237dc |
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)
|
||||
@@ -2028,7 +2028,11 @@ $repeatLabel""")
|
||||
if(asm.isIR)
|
||||
throw AssemblyError("%asm containing IR code cannot be translated to 6502 assembly")
|
||||
else
|
||||
assembly.add(asm.assembly.trimEnd().trimStart('\r', '\n'))
|
||||
// User-supplied assembly should be passed through verbatim. It may contain
|
||||
// characters such as '|' (64tass bitwise OR) that out() would otherwise
|
||||
// misinterpret as a statement separator, and the user's indentation/spacing
|
||||
// should be preserved rather than re-tabbed.
|
||||
assembly.add(asm.assembly.trim('\r', '\n'))
|
||||
}
|
||||
|
||||
private fun translate(incbin: PtIncludeBinary) {
|
||||
|
||||
@@ -169,6 +169,18 @@ internal fun String.hasInstr(mnemonic: String): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
private fun replaceMnemonic(raw: String, oldMnemonic: String, newMnemonic: String): String {
|
||||
val trimmed = raw.trimStart()
|
||||
val instruction = trimmed.instructionPart()
|
||||
if(!instruction.startsWith(oldMnemonic))
|
||||
return raw.replaceFirst(oldMnemonic, newMnemonic)
|
||||
val instrStartInTrimmed = trimmed.indexOf(instruction)
|
||||
if(instrStartInTrimmed<0) return raw.replaceFirst(oldMnemonic, newMnemonic)
|
||||
val leadingLen = raw.length - trimmed.length
|
||||
val idxInRaw = leadingLen + instrStartInTrimmed
|
||||
return raw.substring(0, idxInRaw) + newMnemonic + raw.substring(idxInRaw + oldMnemonic.length)
|
||||
}
|
||||
|
||||
/** Extracts the instruction part of a trimmed assembly line, skipping any label prefix.
|
||||
* For "mylabel: lda #1" returns "lda #1". For "lda #1" returns "lda #1". */
|
||||
private fun String.instructionPart(): String {
|
||||
@@ -673,7 +685,8 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Trimme
|
||||
}
|
||||
else if (tfirst.hasInstr("jsr") && tsecond.hasInstr("rts")) {
|
||||
if(!tfirst.contains("floats.pushFAC") && !tfirst.contains("floats.popFAC")) { // these 2 routines depend on being called with JSR!!
|
||||
mods += Modification(lines[0].index, false, lines[0].value.replace("jsr", "jmp"))
|
||||
val replacement = replaceMnemonic(lines[0].value, "jsr", "jmp")
|
||||
mods += Modification(lines[0].index, false, replacement)
|
||||
mods += Modification(lines[1].index, true, null)
|
||||
}
|
||||
}
|
||||
@@ -750,7 +763,7 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Trimme
|
||||
val firstMnemonic = tfirst.take(3)
|
||||
branchInversions[firstMnemonic]?.let { invertedBranch ->
|
||||
if (sameLabel(tfirst, tsecond, tthird)) {
|
||||
val branch = lines[1].value.replace("jmp", invertedBranch)
|
||||
val branch = replaceMnemonic(lines[1].value, "jmp", invertedBranch)
|
||||
mods.add(Modification(lines[0].index, true, null))
|
||||
mods.add(Modification(lines[1].index, false, branch))
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
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)
|
||||
"msb" -> funcMsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"msb__long" -> funcMsb(fcall, true, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"lsb" -> funcLsb(fcall, false, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"lsb__long" -> funcLsb(fcall,true, firstReturnRegister ?: RegisterOrPair.A)
|
||||
"mkword" -> funcMkword(fcall, firstReturnRegister ?: RegisterOrPair.AY)
|
||||
@@ -2426,55 +2426,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 +2477,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 +2521,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 +2543,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,11 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri
|
||||
}
|
||||
|
||||
|
||||
internal fun deref(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false): Pair<String, UByte> {
|
||||
internal fun deref(pointer: PtPointerDeref, addOffsetToPointer: Boolean=false, loadPointerFieldValue: 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)
|
||||
// loadPointerFieldValue: for addOffsetToPointer=true, also load the value if the final field is a pointer-typed field (rather than returning its address).
|
||||
|
||||
fun addFieldOffsetToScratchPointer(fieldoffset: UInt) {
|
||||
if(fieldoffset==0u)
|
||||
@@ -276,7 +277,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 {
|
||||
@@ -326,6 +327,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 +360,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("""
|
||||
@@ -450,7 +453,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 +465,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}")
|
||||
|
||||
@@ -160,6 +160,20 @@ class TestAsmOptimizer: FunSpec({
|
||||
lines shouldBe listOf(" jsr floats.pushFAC", " rts", " nop", " nop")
|
||||
}
|
||||
|
||||
test("optimizeJsrRts: three consecutive jsr only tail jsr+rts becomes jmp") {
|
||||
val lines = mutableListOf(" jsr \$4000", " jsr \$5000", " jsr \$6000", " rts", " nop", " nop")
|
||||
optimize(lines)
|
||||
lines shouldBe listOf(" jsr \$4000", " jsr \$5000", " jmp \$6000", " nop", " nop")
|
||||
}
|
||||
|
||||
test("optimizeJsrRts: single line with embedded newlines only tail converted defensively") {
|
||||
val lines = mutableListOf(" jsr \$4000\n jsr \$5000\n jsr \$6000", " rts", " nop", " nop")
|
||||
optimize(lines)
|
||||
// Before fix this became jmp/jmp/jmp via replace("jsr","jmp"); after fix only one jmp via replaceMnemonic
|
||||
val jmpCount = lines.joinToString("\n").split("jmp").size - 1
|
||||
jmpCount shouldBe 1
|
||||
}
|
||||
|
||||
// --- optimizeUselessPushPopStack ---
|
||||
|
||||
test("optimizeUselessPushPop: removes phy+ldy+pla") {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,8 @@ import prog8.code.target.VMTarget
|
||||
|
||||
private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.values.isEmpty()
|
||||
|
||||
private class CannotInlineException : Exception()
|
||||
|
||||
/**
|
||||
* The Inliner performs subroutine inlining on the AST.
|
||||
*
|
||||
@@ -87,6 +89,13 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
|
||||
fun isBodyInlineable(stmt: Assignment): Boolean {
|
||||
// Never auto-inline a sub that assigns directly to one of its parameters.
|
||||
// Substituting the call argument into the assignment target would either
|
||||
// crash (literal argument has no identifier to assign to) or change semantics
|
||||
// (assigning to the caller's variable instead of the local parameter copy).
|
||||
val targetName = stmt.target.identifier?.nameInSource?.lastOrNull()
|
||||
if (targetName != null && subroutine.parameters.any { it.name == targetName })
|
||||
return false
|
||||
return if (stmt.value.isSimple) {
|
||||
val targetInline =
|
||||
if (stmt.target.identifier != null) {
|
||||
@@ -139,7 +148,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
stmt.target is NumericLiteral
|
||||
}
|
||||
|
||||
if (!subroutine.isAsmSubroutine && subroutine.parameters.size <= 1) {
|
||||
if (!subroutine.isAsmSubroutine && subroutine.parameters.size <= 1 && subroutine.parameters.all { it.registerOrPair == null }) {
|
||||
val containsSubsOrVariables = subroutine.statements.any { it is Subroutine || (it is VarDecl && it.origin != VarDeclOrigin.SUBROUTINEPARAM) }
|
||||
if (!containsSubsOrVariables) {
|
||||
val functionalStatements = subroutine.statements.filter { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
@@ -254,11 +263,23 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return false to "subroutines with more than 1 parameter cannot be inlined in this compiler version"
|
||||
}
|
||||
|
||||
if (sub.parameters.any { it.registerOrPair != null }) {
|
||||
return false to "subroutines with register-mapped parameters cannot be inlined"
|
||||
}
|
||||
|
||||
if (sub.parameters.size == 1) {
|
||||
val arg = fcall.args.first()
|
||||
if (!arg.isSimple) {
|
||||
return false to "argument is too complex to inline (might have side effects)"
|
||||
}
|
||||
// Refuse to inline a sub that assigns directly to its parameter.
|
||||
// Substituting the argument into the target would crash for a literal
|
||||
// argument, or wrongly modify the caller's variable for a variable argument
|
||||
// (parameters are passed by value, so the sub only modifies its local copy).
|
||||
val paramName = sub.parameters[0].name
|
||||
val bodyStmt = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM }
|
||||
if ((bodyStmt as? Assignment)?.target?.identifier?.nameInSource?.lastOrNull() == paramName)
|
||||
return false to "cannot inline subroutine that assigns to its parameter"
|
||||
}
|
||||
|
||||
if (options.compTarget.name != VMTarget.NAME) {
|
||||
@@ -304,8 +325,9 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
fun inlineFunctionBody(toInline: Return): Iterable<AstModification> {
|
||||
// call site is an expression, so we have to have a Return here in the inlined sub to provide the values
|
||||
return if (toInline.values.size == 1 && functionCallExpr !== toInline.values[0]) {
|
||||
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as? Expression
|
||||
?: return noModifications
|
||||
sub.hasBeenInlined = true
|
||||
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as Expression
|
||||
listOf(AstReplaceNode(functionCallExpr, substitutedReturn, parent))
|
||||
} else
|
||||
noModifications
|
||||
@@ -326,7 +348,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
|
||||
fun possiblyShortCircuitFunctionCall(toInline: Return): Iterable<AstModification> {
|
||||
// Substitute parameters in the return values first
|
||||
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as Return
|
||||
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as? Return
|
||||
?: return noModifications
|
||||
val functionCalls = substitutedReturn.values.filterIsInstance<FunctionCallExpression>()
|
||||
|
||||
if (functionCalls.isEmpty()) {
|
||||
@@ -353,7 +376,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
|
||||
fun possiblyInlineFunctionBody(toInline: Statement): Iterable<AstModification> {
|
||||
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as Statement
|
||||
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as? Statement
|
||||
?: return noModifications
|
||||
|
||||
return if (origNode !== toInline) {
|
||||
sub.hasBeenInlined = true
|
||||
@@ -425,7 +449,8 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return noModifications
|
||||
|
||||
// Substitute parameters in all return values
|
||||
val substitutedReturn = substituteParameters(sub, fcall, toInline) as Return
|
||||
val substitutedReturn = substituteParameters(sub, fcall, toInline) as? Return
|
||||
?: return noModifications
|
||||
|
||||
// Create multiple single assignments, skipping void targets (they discard the value)
|
||||
val newAssignments = multiTargets.zip(substitutedReturn.values)
|
||||
@@ -452,7 +477,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return listOf(AstReplaceNode(assignment, scope, parent))
|
||||
}
|
||||
|
||||
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node {
|
||||
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node? {
|
||||
val paramVarDecls = sub.parameters.map { param ->
|
||||
sub.statements.filterIsInstance<VarDecl>().find { it.origin == VarDeclOrigin.SUBROUTINEPARAM && it.name == param.name }
|
||||
?: throw FatalAstException("parameter ${param.name} not found in subroutine ${sub.name}")
|
||||
@@ -475,7 +500,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
is ArrayIndexedExpression -> ArrayIndexedExpression(
|
||||
n.plainarrayvar?.let {
|
||||
val subId = substitute(it)
|
||||
if (subId !is IdentifierReference) throw FatalAstException("parameter substituted by ${subId::class.simpleName} but an identifier reference was expected")
|
||||
if (subId !is IdentifierReference) throw CannotInlineException()
|
||||
subId
|
||||
},
|
||||
n.nestedArray?.let { substitute(it) as ArrayIndexedExpression },
|
||||
@@ -489,7 +514,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
val newTarget = n.target.copy()
|
||||
newTarget.identifier = n.target.identifier?.let {
|
||||
val subId = substitute(it)
|
||||
if (subId !is IdentifierReference) throw FatalAstException("parameter substituted by ${subId::class.simpleName} but an identifier reference was expected")
|
||||
if (subId !is IdentifierReference) throw CannotInlineException()
|
||||
subId
|
||||
}
|
||||
newTarget.memoryAddress = n.target.memoryAddress?.let { DirectMemoryWrite(substitute(it.addressExpression) as Expression, it.position) }
|
||||
@@ -512,7 +537,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
is FunctionCallExpression -> FunctionCallExpression(
|
||||
substitute(n.target).let {
|
||||
if (it !is IdentifierReference) throw FatalAstException("parameter substituted by ${it::class.simpleName} but an identifier reference was expected")
|
||||
if (it !is IdentifierReference) throw CannotInlineException()
|
||||
it
|
||||
},
|
||||
n.args.map { substitute(it) as Expression }.toMutableList(),
|
||||
@@ -520,7 +545,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
)
|
||||
is FunctionCallStatement -> FunctionCallStatement(
|
||||
substitute(n.target).let {
|
||||
if (it !is IdentifierReference) throw FatalAstException("parameter substituted by ${it::class.simpleName} but an identifier reference was expected")
|
||||
if (it !is IdentifierReference) throw CannotInlineException()
|
||||
it
|
||||
},
|
||||
n.args.map { substitute(it) as Expression }.toMutableList(),
|
||||
@@ -536,7 +561,7 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
is AddressOf -> AddressOf(
|
||||
n.identifier?.let {
|
||||
val subId = substitute(it)
|
||||
if (subId !is IdentifierReference) throw FatalAstException("parameter substituted by ${subId::class.simpleName} but an identifier reference was expected")
|
||||
if (subId !is IdentifierReference) throw CannotInlineException()
|
||||
subId
|
||||
},
|
||||
n.arrayIndex?.let { substitute(it) as ArrayIndex },
|
||||
@@ -549,6 +574,10 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
}
|
||||
|
||||
return substitute(node)
|
||||
return try {
|
||||
substitute(node)
|
||||
} catch (_: CannotInlineException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -538,6 +538,8 @@ internal fun determineCompilationOptions(program: Program, compTarget: ICompilat
|
||||
}
|
||||
|
||||
private fun processAst(program: Program, errors: IErrorReporter, compilerOptions: CompilationOptions) {
|
||||
program.checkVarDeclsOnOwnLine(errors)
|
||||
errors.report()
|
||||
program.preprocessAst(errors, compilerOptions)
|
||||
if(errors.noErrors() && compilerOptions.dumpSymbols) {
|
||||
printSymbols(program)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,10 +5,7 @@ import prog8.ast.Node
|
||||
import prog8.ast.Program
|
||||
import prog8.ast.expressions.*
|
||||
import prog8.ast.statements.*
|
||||
import prog8.ast.walk.AstModification
|
||||
import prog8.ast.walk.AstRemove
|
||||
import prog8.ast.walk.AstReplaceNode
|
||||
import prog8.ast.walk.AstWalker
|
||||
import prog8.ast.walk.*
|
||||
import prog8.code.core.*
|
||||
import java.io.CharConversionException
|
||||
|
||||
@@ -24,8 +21,47 @@ internal fun invalidNumberOfArgsError(errors: IErrorReporter, pos: Position, num
|
||||
errors.err("invalid number of arguments: expected $expected but got $numArgs", pos)
|
||||
}
|
||||
|
||||
internal fun Program.checkValid(errors: IErrorReporter, compilerOptions: CompilationOptions) {
|
||||
// semantic analysis to see if the program is valid.
|
||||
internal fun Program.checkVarDeclsOnOwnLine(errors: IErrorReporter) {
|
||||
// A variable declaration directly followed by another statement on the same
|
||||
// source line (e.g. `ubyte length foo(42)`) parses as two statements, but it is
|
||||
// almost certainly a missing '=' or newline and very confusing, so report it.
|
||||
// This must run on the freshly parsed tree: later passes split off initializer
|
||||
// assignments and remove unused variables, which destroys the original adjacency.
|
||||
fun checkStatements(statements: List<Statement>) {
|
||||
for (i in 0 until statements.size - 1) {
|
||||
val cur = statements[i] as? VarDecl ?: continue
|
||||
if (cur.origin != VarDeclOrigin.USERCODE) continue
|
||||
val next = statements[i + 1]
|
||||
val curPos = cur.position
|
||||
val nextPos = next.position
|
||||
if (curPos.file == nextPos.file && curPos.line != 0 && curPos.line == nextPos.line && nextPos.startCol > curPos.endCol) {
|
||||
errors.err("variable declaration must be on its own line (did you forget '=' or a newline?)", nextPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val checker = object : IAstVisitor {
|
||||
override fun visit(block: Block) {
|
||||
checkStatements(block.statements)
|
||||
super.visit(block)
|
||||
}
|
||||
override fun visit(subroutine: Subroutine) {
|
||||
checkStatements(subroutine.statements)
|
||||
super.visit(subroutine)
|
||||
}
|
||||
override fun visit(scope: AnonymousScope) {
|
||||
checkStatements(scope.statements)
|
||||
super.visit(scope)
|
||||
}
|
||||
override fun visit(decl: VarDecl) {
|
||||
// Do not traverse into the declaration: its value can still be an
|
||||
// ExpressionTuple at this stage, which cannot be visited.
|
||||
}
|
||||
}
|
||||
checker.visit(this)
|
||||
}
|
||||
|
||||
internal fun Program.checkValid(errors: IErrorReporter, compilerOptions: CompilationOptions) { // semantic analysis to see if the program is valid.
|
||||
val parentChecker = ParentNodeChecker()
|
||||
parentChecker.visit(this)
|
||||
val checker = AstChecker(this, errors, compilerOptions)
|
||||
|
||||
@@ -11,6 +11,7 @@ import prog8.ast.statements.*
|
||||
import prog8.ast.walk.IAstVisitor
|
||||
import prog8.code.core.*
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.parser.Prog8Keywords
|
||||
|
||||
/**
|
||||
* This checks for naming conflicts, not for correct symbol references yet.
|
||||
@@ -30,15 +31,21 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
}
|
||||
|
||||
override fun visit(block: Block) {
|
||||
val existing = blocks[block.name]
|
||||
if(existing!=null) {
|
||||
if (block.isInLibrary)
|
||||
nameError(existing.name, existing.position, block)
|
||||
if(block.name in BuiltinFunctions)
|
||||
errors.err("builtin function cannot be redefined", block.position)
|
||||
else if(block.name in keywords)
|
||||
errors.err("block name cannot be a keyword", block.position)
|
||||
else {
|
||||
val existing = blocks[block.name]
|
||||
if(existing!=null) {
|
||||
if (block.isInLibrary)
|
||||
nameError(existing.name, existing.position, block)
|
||||
else
|
||||
nameError(block.name, block.position, existing)
|
||||
}
|
||||
else
|
||||
nameError(block.name, block.position, existing)
|
||||
blocks[block.name] = block
|
||||
}
|
||||
else
|
||||
blocks[block.name] = block
|
||||
|
||||
super.visit(block)
|
||||
}
|
||||
@@ -79,41 +86,8 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
super.visit(decl)
|
||||
}
|
||||
|
||||
private val keywords = setOf(
|
||||
"void",
|
||||
"on",
|
||||
"in",
|
||||
"to",
|
||||
"if",
|
||||
"downto",
|
||||
"else",
|
||||
"step",
|
||||
"call",
|
||||
"goto",
|
||||
"inline",
|
||||
"struct",
|
||||
"then",
|
||||
"alias",
|
||||
"defer",
|
||||
"const",
|
||||
"sizeof",
|
||||
"as",
|
||||
"return",
|
||||
"break",
|
||||
"continue",
|
||||
"true",
|
||||
"false",
|
||||
"sub",
|
||||
"asmsub",
|
||||
"extsub",
|
||||
"clobbers",
|
||||
"while",
|
||||
"do",
|
||||
"until",
|
||||
"repeat",
|
||||
"unroll",
|
||||
"when"
|
||||
)
|
||||
// the set of reserved keywords in the prog8 language
|
||||
private val keywords: Set<String> by lazy { Prog8Keywords.reservedWords }
|
||||
|
||||
override fun visit(struct: StructDecl) {
|
||||
if(struct.name in BuiltinFunctions)
|
||||
@@ -121,7 +95,6 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
else if(struct.name in keywords)
|
||||
errors.err("struct name cannot be a keyword", struct.position)
|
||||
|
||||
|
||||
super.visit(struct)
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -694,4 +694,122 @@ main {
|
||||
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
|
||||
errors.printedErrors.any { it.contains("2D arrays are not allowed as struct fields") } shouldBe true
|
||||
}
|
||||
|
||||
test("struct field array size can be const expression") {
|
||||
val src = """
|
||||
main {
|
||||
const ubyte SIZE = 5
|
||||
struct Node {
|
||||
ubyte[SIZE] data
|
||||
uword top
|
||||
}
|
||||
sub start() {
|
||||
^^Node n = [ [1,2,3,4,5], 9999 ]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors shouldBe emptyList()
|
||||
}
|
||||
|
||||
test("struct field array size can be folded constant expression") {
|
||||
val src = """
|
||||
main {
|
||||
const ubyte BASE = 3
|
||||
struct Node {
|
||||
ubyte[BASE*2] data
|
||||
}
|
||||
sub start() {
|
||||
^^Node n = [ [1,2,3,4,5,6] ]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors shouldBe emptyList()
|
||||
}
|
||||
|
||||
test("struct field array size rejects non-constant expression") {
|
||||
val src = """
|
||||
main {
|
||||
uword dyn
|
||||
struct Node {
|
||||
uword[dyn] data
|
||||
}
|
||||
sub start() {
|
||||
dyn = 5
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
|
||||
errors.errors.any { it.contains("array length must be known at compile-time") } shouldBe true
|
||||
}
|
||||
|
||||
test("struct field array size rejects zero size") {
|
||||
val src = """
|
||||
main {
|
||||
const ubyte SIZE = 0
|
||||
struct Node {
|
||||
ubyte[SIZE] data
|
||||
}
|
||||
sub start() { }
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
|
||||
errors.errors.any { it.contains("array length must be positive") } shouldBe true
|
||||
}
|
||||
|
||||
test("variable declaration followed by statement on same line should give error") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
ubyte length foo(42)
|
||||
}
|
||||
sub foo(ubyte x) {
|
||||
x++
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "must be on its own line"
|
||||
}
|
||||
|
||||
test("variable declaration followed by statement on next line is fine") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
ubyte length
|
||||
length = 42
|
||||
foo(length)
|
||||
}
|
||||
sub foo(ubyte x) {
|
||||
x++
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false) shouldNotBe null
|
||||
errors.errors.size shouldBe 0
|
||||
}
|
||||
|
||||
test("two statements on same line is fine if first one is not a variable declaration") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
foo(1) foo(2)
|
||||
}
|
||||
sub foo(ubyte x) {
|
||||
cx16.r0 = x
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false) shouldNotBe null
|
||||
errors.errors.size shouldBe 0
|
||||
}
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -1399,6 +1399,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
|
||||
|
||||
@@ -643,6 +643,54 @@ main {
|
||||
lines.any { it.startsWith("jsr") && it.contains("p8s_test") } shouldBe true
|
||||
}
|
||||
|
||||
test("inline asm pipe operator is preserved as bitwise OR") {
|
||||
val src="""
|
||||
%zeropage basicsafe
|
||||
%option no_sysinit
|
||||
main {
|
||||
sub start() {
|
||||
%asm {{
|
||||
lda #%00010000 | $1234>>16
|
||||
}}
|
||||
}
|
||||
}"""
|
||||
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
|
||||
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
|
||||
val lines = asm.lines().map { it.trim() }
|
||||
// the '|' must remain on the same line as the lda, not be split into a second line
|
||||
lines.any { it == "lda #%00010000 | $1234>>16" } shouldBe true
|
||||
lines.any { it == "$1234>>16" } shouldBe false
|
||||
}
|
||||
|
||||
test("subroutine with register-mapped parameter is not inlined") {
|
||||
val src= $$"""
|
||||
%zeropage basicsafe
|
||||
%option no_sysinit
|
||||
main {
|
||||
sub start() {
|
||||
PlayVideo_STD(mkword($10, $20))
|
||||
}
|
||||
|
||||
sub PlayVideo_STD(uword params @R0) {
|
||||
TrampolineCallSTD($a000)
|
||||
}
|
||||
|
||||
asmsub TrampolineCallSTD(uword addr @AY) clobbers(A, Y) {
|
||||
%asm {{
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}"""
|
||||
val result = compileText(C64Target(), false, src, outputDir, writeAssembly = true)!!
|
||||
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
|
||||
val lines = asm.lines().map { it.trim() }
|
||||
// the R0 argument setup must be preserved because the inlined body doesn't
|
||||
// explicitly reference the register-mapped parameter
|
||||
lines.any { it == "sta cx16.r0" } shouldBe true
|
||||
lines.any { it == "sty cx16.r0+1" } shouldBe true
|
||||
lines.any { it.startsWith("jsr") && it.contains("p8s_PlayVideo_STD") } shouldBe true
|
||||
}
|
||||
|
||||
test("if not without else is not swapped") {
|
||||
val src="""
|
||||
main {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -380,6 +380,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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+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.2
|
||||
version=12.3.4
|
||||
Reference in new issue
Block a user