Compare commits

..
7 Commits
Author SHA1 Message Date
Irmen de Jong ee09d35fdc fix byte 'downto 0' for-loop hanging on the virtual target
BSTPOS after DECM read stale status flags on the VM because DECM leaves
flags untouched there. Only use the BSTPOS fast path on 6502 hardware or
targets honoring the multi-byte status-bits contract; otherwise emit the
explicit LOADM+CMPI+BSTNE check. Backported from master (22a2f8cbc).
2026-09-12 01:36:51 +02:00
Irmen de Jong af14562e2b allow struct field array sizes to be constant expressions
fixes #240
2026-09-11 01:38:18 +02:00
Irmen de Jong ccc26a0eb2 Fix alias targets to enum members
Defer alias error reporting when the target is a not-yet-desugared
enum member. Also reject aliases that target an enum itself.

fixes #239
2026-09-10 19:20:02 +02:00
Irmen de Jong e017c541bb proper error message when using a builtin function name as block name. Also for some keywords if the parser allows. Fixes #236 2026-09-10 19:20:02 +02:00
Irmen de Jong 3d5a7795d0 make set of keywords (for name restriction test) dynamic 2026-09-10 19:20:02 +02:00
Irmen de Jong af06fdfffc fix more 6502-only struct pointer-field bugs; add indexed in-place support
fixes #237
2026-09-10 19:20:02 +02:00
Irmen de Jong e811ecdbde fix 6502 struct pointer-field array indexing 2026-09-09 23:47:09 +02:00
25 changed files with 506 additions and 136 deletions

No files matched your search

@@ -44,6 +44,11 @@ enum class CpuType(val statusBitsOnMultiByteOps: Boolean) {
* behavior consistent across all current targets.
*/
VIRTUAL(false)
;
/** True for the 6502-family targets (plain 6502 and 65C02). */
val is6502: Boolean
get() = this == CPU6502 || this == CPU65C02
}
interface ICompilationTarget: IStringEncoding, IMemSizer {
@@ -1498,7 +1498,7 @@ $repeatLabel""")
}
else if(swap.target1.pointerDeref!=null && swap.target2.pointerDeref!=null) {
val v1 = swap.target1.pointerDeref!!
val v2 = swap.target1.pointerDeref!!
val v2 = swap.target2.pointerDeref!!
if(v1.derefLast && v1.chain.isEmpty() && v2.derefLast && v2.chain.isEmpty() && isZpVar(v1.startpointer) && isZpVar(v2.startpointer)) {
// optimized case where v1 and v2 are both already zeropage pointer variables
val name1 = asmVariableName(v1.startpointer)
@@ -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}")
@@ -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)
@@ -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) {
@@ -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 {
+67
View File
@@ -695,6 +695,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 {
+55
View File
@@ -664,6 +664,61 @@ main {
}
test("alias to enum member works") {
val src = """
main {
enum Priority { LOW, NORMAL, HIGH, EXTREME }
alias myNormal = Priority::NORMAL
alias chainedNormal = myNormal
sub start() {
ubyte @shared value1 = Priority::NORMAL
ubyte @shared value2 = myNormal
ubyte @shared value3 = chainedNormal
}
}"""
val result = compileText(Cx16Target(), false, src, outputDir, writeAssembly = false)!!
val start = result.compilerAst.allBlocks.first { it.name == "main" }.statements.filterIsInstance<prog8.ast.statements.Subroutine>().first { it.name == "start" }
val assignments = start.statements.filterIsInstance<Assignment>()
val value1 = (assignments[0].value as? NumericLiteral)?.number
val value2 = (assignments[1].value as? NumericLiteral)?.number
val value3 = (assignments[2].value as? NumericLiteral)?.number
value1 shouldBe 1.0
value2 shouldBe 1.0
value3 shouldBe 1.0
}
test("alias to whole enum gives error") {
val src = """
main {
enum Priority { LOW, NORMAL, HIGH, EXTREME }
alias myEnum = Priority
sub start() {
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "cannot alias an enum"
}
test("alias to nonexistent enum member gives undefined error") {
val src = """
main {
enum Priority { LOW, NORMAL, HIGH, EXTREME }
alias myBad = Priority::NONEXISTENT
sub start() {
}
}"""
val errors = ErrorReporterForTests()
compileText(Cx16Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 1
errors.errors[0] shouldContain "undefined symbol: Priority::NONEXISTENT"
}
test("double colon in name only allowed in desugared enum members 2") {
val src = """
main {
@@ -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
@@ -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
}
}
+2 -1
View File
@@ -417,7 +417,8 @@ set it all to the same value, or maybe even simply assign the individual element
Note that the various keywords for the data type and variable type (``byte``, ``word``, ``const``, etc.)
can't be used as *identifiers* elsewhere. You can't make a variable, block or subroutine with the name ``byte``
for instance.
for instance. The same applies to the names of the builtin functions (such as ``memory``, ``mkword``,
``peek`` and ``poke``): they cannot be redefined as a variable, block, subroutine, struct or label.
Using the ``in`` operator you can easily check if a value is present in an array,
example: ``if choice in [1,2,3,4] {....}``
+7 -17
View File
@@ -1,22 +1,12 @@
%import textio
%zeropage basicsafe
main {
struct Thing {
uword port
^^Thing next
bool flag
}
sub start() {
^^Thing @shared t
; this compiles fine:
bool @shared derp1 = t.next.flag
ubyte @shared derp2 = t.next.flag as ubyte
bool @shared derp3 = (t.next).flag
ubyte @shared derp4 = (t.next).flag as ubyte
bool @shared derp5 = (t.port as ^^Thing).flag
ubyte @shared derp6 = (t.port as ^^Thing).flag as ubyte
for cx16.r0L in 5 downto 0 {
txt.print_ub(cx16.r0L)
txt.spc()
}
}
}
+1 -1
View File
@@ -4,4 +4,4 @@ org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.configuration-cache=false
kotlin.code.style=official
version=12.3.3
version=12.3.4