Add arrays of struct instances (StructType[N] vars)

Parser: accept `Type[N] name` between type and identifier
(Prog8ANTLR.g4, Antlr2KotlinVisitor) via elementToArray()
for STRUCT_INSTANCE; CodeDesugarer: handle struct-instance
bases, addressOfStructInstance(), whole-struct memcopy
(points[a]=points[b]), and fix variable-index 16-bit scaling
in 6502 AssignmentAsmGen.computeStructArrayElementAddress();
IR: preserve STRUCTDEFS round-trip, VM loader support;
m68k/new6502/intermediate backends; docs + todo updated.
Limitations: not as struct fields (AstChecker/
AstIdentifiersChecker error) nor as sub params.
This commit is contained in:
Irmen de Jong committed 2026-08-30 14:14:49 +02:00
1 parent b0d50c2a68
commit 94822b8662
26 files changed
+887 -293

No files matched your search

+27 -10
View File
@@ -201,7 +201,7 @@ class DataType private constructor(
fun forDt(dt: BaseDataType): DataType {
if(dt.isStructInstance)
TODO("cannot use struct instance as a data type (yet) - use a pointer instead")
return DataType(BaseDataType.STRUCT_INSTANCE, null, null)
return simpletypes.getValue(dt)
}
@@ -229,6 +229,11 @@ class DataType private constructor(
fun arrayOfPointersFromAntlrTo(sub: BaseDataType?, identifier: List<String>?): DataType =
DataType(BaseDataType.ARRAY_POINTER, sub, null, identifier)
// array of struct instances: sub is STRUCT_INSTANCE, subType carries the resolved struct
fun arrayOfStructs(structType: ISubType): DataType = DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, structType)
fun arrayOfStructsFromAntlr(struct: List<String>): DataType =
DataType(BaseDataType.ARRAY, BaseDataType.STRUCT_INSTANCE, null, subTypeFromAntlr = struct)
fun pointer(base: BaseDataType): DataType = DataType(BaseDataType.POINTER, base, null)
fun pointer(dt: DataType): DataType = if(dt.isBasic)
DataType(BaseDataType.POINTER, dt.base, null)
@@ -246,26 +251,34 @@ class DataType private constructor(
// ============================================================================
fun elementToArray(target: ICompilationTarget, splitwords: Boolean = true): DataType {
fun pointerArray(): DataType =
if (subType != null)
fun pointerArray(): DataType =
if (subType != null)
arrayOfPointersTo(subType)
else if (subTypeFromAntlr != null)
else if (subTypeFromAntlr != null)
arrayOfPointersFromAntlrTo(sub, subTypeFromAntlr)
else
else
arrayOfPointersTo(sub!!)
if(base == BaseDataType.STRUCT_INSTANCE) {
val st = subType
return if(st != null)
arrayOfStructs(st)
else
arrayOfStructsFromAntlr(subTypeFromAntlr!!)
}
if(!target.cpu.is6502) {
return if (base.isPointer)
return if (base.isPointer)
pointerArray()
else
else
arrayFor(base, target)
}
return if (splitwords && (base == BaseDataType.UWORD || base == BaseDataType.WORD || base == BaseDataType.STR))
splitWordArrayFor(base)
else if(base.isPointer)
else if(base.isPointer)
pointerArray()
else
else
arrayFor(base, target)
}
@@ -273,6 +286,8 @@ class DataType private constructor(
when {
isPointerArray -> DataType(BaseDataType.POINTER, sub, subType)
base.isArray || base==BaseDataType.STR -> {
if (sub == BaseDataType.STRUCT_INSTANCE && subType != null)
return DataType(BaseDataType.STRUCT_INSTANCE, null, subType)
val dt = forDt(sub!!)
if (subType != null) DataType(BaseDataType.POINTER, null, subType) else dt
}
@@ -327,6 +342,7 @@ class DataType private constructor(
BaseDataType.UBYTE -> "ubyte[]"
BaseDataType.UWORD -> "uword[]"
BaseDataType.LONG -> "long[]"
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[]" else "$subTypeFromAntlr[]"
else -> throw IllegalArgumentException("invalid sub type")
}
}
@@ -391,6 +407,7 @@ class DataType private constructor(
BaseDataType.WORD -> "@nosplit word["
BaseDataType.LONG -> "long["
BaseDataType.FLOAT -> "float["
BaseDataType.STRUCT_INSTANCE -> if (subType != null) "${subType!!.scopedNameString}[" else "$subTypeFromAntlr["
else -> throw IllegalArgumentException("invalid sub type")
}
}
@@ -20,6 +20,7 @@ internal class NormalMemSizer(override val FLOAT_MEM_SIZE: UInt, override val PO
BaseDataType.STR, BaseDataType.POINTER -> numElements * pointerSize
BaseDataType.LONG -> numElements * 4
BaseDataType.FLOAT-> numElements * floatSize
BaseDataType.STRUCT_INSTANCE -> numElements * (dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance array has unresolved subtype"))
BaseDataType.UNDEFINED -> throw IllegalArgumentException("undefined has no memory size")
else -> throw IllegalArgumentException("invalid sub type")
}
@@ -35,7 +36,7 @@ internal class NormalMemSizer(override val FLOAT_MEM_SIZE: UInt, override val PO
dt.isLong -> 4 * (numElements ?: 1)
dt.isWord -> 2 * (numElements ?: 1)
dt.isPointer -> pointerSize * (numElements ?: 1)
dt.isStructInstance -> dt.subType?.memsize(this) ?: 0
dt.isStructInstance -> dt.subType?.memsize(this) ?: throw IllegalArgumentException("struct instance has unresolved subtype")
dt.isUndefined -> throw IllegalArgumentException("undefined has no memory size")
else -> throw IllegalArgumentException("invalid dt $dt")
}
@@ -127,10 +127,10 @@ class TestDataType: FunSpec({
DataType.forDt(BaseDataType.FLOAT) shouldBe DataType.FLOAT
}
test("DataType.forDt throws for struct instance") {
shouldThrow<NotImplementedError> {
DataType.forDt(BaseDataType.STRUCT_INSTANCE)
}
test("DataType.forDt supports struct instance") {
val dt = DataType.forDt(BaseDataType.STRUCT_INSTANCE)
dt.base shouldBe BaseDataType.STRUCT_INSTANCE
dt.isStructInstance shouldBe true
}
test("DataType.arrayFor creates array types") {
@@ -2247,6 +2247,15 @@ $repeatLabel""")
val constOffset = (ptrAndIndex.second as? PtNumber)?.number?.toInt()
if(addrOf!=null && constOffset!=null) {
if(addrOf.isFromArrayElement) {
val arrayId = addrOf.identifier
val arrayDt = arrayId?.type
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
out(" pha")
assignmentAsmGen.computeStructArrayElementAddress(arrayDt, asmSymbolName(arrayId), addrOf.arrayIndexExpr!!, "P8ZP_SCRATCH_PTR")
out(" pla")
out(" ldy #$constOffset | sta (P8ZP_SCRATCH_PTR),y")
return true
}
TODO("address-of array element ${addrOf.position}")
} else if(addrOf.dereference!=null) {
throw AssemblyError("write &dereference, makes no sense at ${addrOf.position}")
@@ -2293,6 +2302,13 @@ $repeatLabel""")
val constOffset = (ptrAndIndex.second as? PtNumber)?.number?.toInt()
if(addrOf!=null && constOffset!=null) {
if(addrOf.isFromArrayElement) {
val arrayId = addrOf.identifier
val arrayDt = arrayId?.type
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
assignmentAsmGen.computeStructArrayElementAddress(arrayDt, asmSymbolName(arrayId), addrOf.arrayIndexExpr!!, "P8ZP_SCRATCH_PTR")
out(" ldy #$constOffset | lda (P8ZP_SCRATCH_PTR),y")
return true
}
TODO("address-of array element ${addrOf.position}")
} else if(addrOf.dereference!=null) {
if(pointerGen.readByteByAddressOfDereference(addrOf, constOffset))
@@ -419,7 +419,7 @@ internal class ProgramAndVarsGen(
asmgen.out("${StStructInstanceBlockName}_bss .block\n")
instancesNoInit.forEach {
val structtype: StStruct = symboltable.lookup(it.structName) as StStruct
asmgen.out("${it.name} .fill ${structtype.size}\n")
asmgen.out("${it.name} .fill ${structtype.size} ; struct ${it.structName}\n")
}
asmgen.out(" .endblock\n")
asmgen.out(" .send BSS\n")
@@ -789,13 +789,14 @@ internal class ProgramAndVarsGen(
dt.isSplitWordArray(compTarget) -> {
alignVar(variable.align)
val numbytesPerHalf = compTarget.memorySize(variable.dt, variable.length!!.toInt()) / 2
asmgen.out("${variable.name}_lsb\t.fill $numbytesPerHalf")
asmgen.out("${variable.name}_msb\t.fill $numbytesPerHalf")
asmgen.out("${variable.name}_lsb\t.fill $numbytesPerHalf ; split word array of ${variable.length} ${variable.dt.elementType()} (lsbs)")
asmgen.out("${variable.name}_msb\t.fill $numbytesPerHalf ; split word array of ${variable.length} ${variable.dt.elementType()} (msbs)")
}
dt.isArray -> {
alignVar(variable.align)
val numbytes = compTarget.memorySize(variable.dt, variable.length!!.toInt())
asmgen.out("${variable.name}\t.fill $numbytes")
val amount = variable.length!!.toInt()
val elementSize = compTarget.memorySize(variable.dt.elementType(), null)
asmgen.out("${variable.name}\t.fill $amount * $elementSize ; array of $amount ${variable.dt.elementType()} ")
}
dt.isPointer -> asmgen.out("${variable.name}\t.word ?") // a pointer is just an uword address
dt.isPointerArray -> {
@@ -931,6 +932,46 @@ internal class ProgramAndVarsGen(
for (f in array.zip(floatFills))
asmgen.out(" .byte ${f.second} ; float ${f.first}")
}
dt.base==BaseDataType.ARRAY && dt.sub==BaseDataType.STRUCT_INSTANCE -> {
val struct = dt.subType as? StStruct ?: throw AssemblyError("struct type not found for $varname")
val structSize = struct.size.toInt()
if(value==null) {
val totalBytes = (orNumberOfZeros ?: 0) * structSize
if(totalBytes==0) {
asmgen.out("$varname: .byte 0")
} else {
asmgen.out("$varname: .fill $totalBytes, 0")
}
} else {
// initialized struct array: value contains StructInstance elements
asmgen.out(varname)
// expand struct field types for per-element handling
val expandedFieldTypes = struct.fields.flatMap { field ->
val arraySz = field.arraySize
if(arraySz!=null) List(arraySz) { field.type } else listOf(field.type)
}
for(elt in value) {
val instanceName = (elt as? StArrayElement.StructInstance)?.name ?: throw AssemblyError("expected struct instance in array init for $varname")
val instance = symboltable.lookup(instanceName) as? StStructInstance ?: throw AssemblyError("struct instance $instanceName not found")
val fieldValues = instance.initialValues
require(fieldValues.size==expandedFieldTypes.size) { "field count mismatch for $instanceName" }
for((fieldType, fieldVal) in expandedFieldTypes.zip(fieldValues)) {
val num = (fieldVal as? StArrayElement.Number)?.value ?: throw AssemblyError("struct field value must be number for $varname")
when {
fieldType.isByteOrBool -> asmgen.out(" .byte ${num.toInt()}")
fieldType.isWord -> asmgen.out(" .word ${num.toInt()}")
fieldType.isLong -> asmgen.out(" .dword ${num.toInt()}")
fieldType.isFloat -> {
val bytes = compTarget.getFloatAsmBytes(num)
asmgen.out(" .byte $bytes ; float $num")
}
fieldType.isPointer -> asmgen.out(" .word ${num.toInt()} ; pointer")
else -> throw AssemblyError("unsupported struct field type $fieldType")
}
}
}
}
}
else -> throw AssemblyError("require array dt")
}
}
@@ -3945,6 +3945,65 @@ $endLabel""")
}
}
/**
* Compute the address of a struct-array element with a variable index into a zeropage pointer.
* Uses full 16-bit scaling so it works for indices whose offset exceeds 255 bytes.
*/
internal fun computeStructArrayElementAddress(arrayDt: DataType, arraySymbol: String, indexExpr: PtExpression, destPtrVar: String = "P8ZP_SCRATCH_PTR") {
require(arrayDt.sub == BaseDataType.STRUCT_INSTANCE) { "expected struct instance array $arrayDt" }
val structSize = try {
(arrayDt.subType as? StStruct)?.size?.toInt() ?: program.memsizer.memorySize(arrayDt.elementType(), 1)
} catch(_: Exception) {
program.memsizer.memorySize(arrayDt.elementType(), 1)
}.toUInt()
// load index as word
asmgen.assignExpressionToVariable(indexExpr, "P8ZP_SCRATCH_W1", DataType.UWORD)
// scale by struct size (preserve full 16 bits)
when(structSize) {
1u -> {}
2u -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
4u -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
8u -> asmgen.out(" asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1 | asl P8ZP_SCRATCH_W1 | rol P8ZP_SCRATCH_W1+1")
else -> {
if(structSize.toInt() in asmgen.optimizedWordMultiplications) {
asmgen.out("""
lda P8ZP_SCRATCH_W1
ldy P8ZP_SCRATCH_W1+1
jsr prog8_math.mul_word_${structSize}
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1""")
} else {
asmgen.out("""
lda P8ZP_SCRATCH_W1
ldy P8ZP_SCRATCH_W1+1
sta prog8_math.multiply_words.multiplier
sty prog8_math.multiply_words.multiplier+1
lda #<$structSize
ldy #>$structSize
jsr prog8_math.multiply_words
sta P8ZP_SCRATCH_W1
sty P8ZP_SCRATCH_W1+1""")
}
}
}
asmgen.out("""
lda #<$arraySymbol
sta $destPtrVar
lda #>$arraySymbol
sta $destPtrVar+1
clc
lda $destPtrVar
adc P8ZP_SCRATCH_W1
sta $destPtrVar
lda $destPtrVar+1
adc P8ZP_SCRATCH_W1+1
sta $destPtrVar+1""")
}
private fun assignAddressOf(target: AsmAssignTarget, sourceName: String, msb: Boolean, arrayDt: DataType?, arrayIndexExpr: PtExpression?) {
var actualSourceName = sourceName
var actualMsb = msb
@@ -4049,6 +4108,13 @@ $endLabel""")
clc
adc P8ZP_SCRATCH_REG"""
)
} else if(subtype==BaseDataType.STRUCT_INSTANCE) {
// Use the full 16-bit address computation helper. It evaluates the index itself,
// so the earlier load into A is not needed for this path.
computeStructArrayElementAddress(arrayDt, actualSourceName, arrayIndexExpr, "P8ZP_SCRATCH_PTR")
asmgen.out(" lda P8ZP_SCRATCH_PTR | ldy P8ZP_SCRATCH_PTR+1")
assignRegisterpairWord(target, RegisterOrPair.AY)
return
} else throw AssemblyError("weird type $subtype")
asmgen.out("""
ldy #>$actualSourceName
@@ -5860,7 +5926,16 @@ $endLabel""")
}
addressOf != null -> {
if(addressOf.isFromArrayElement) {
TODO("address-of array element $addressOf")
val arrayId = addressOf.identifier
val arrayDt = arrayId?.type
if(arrayDt?.sub == BaseDataType.STRUCT_INSTANCE) {
if(byteValue==null) asmgen.out(" pha")
computeStructArrayElementAddress(arrayDt, asmgen.asmSymbolName(arrayId), addressOf.arrayIndexExpr!!, "P8ZP_SCRATCH_PTR")
if(byteValue==null) asmgen.out(" pla") else asmgen.out(" lda #${byteValue.toHex()}")
asmgen.out(" ldy #0 | sta (P8ZP_SCRATCH_PTR),y")
} else {
TODO("address-of array element $addressOf")
}
} else if(addressOf.dereference!=null) {
throw AssemblyError("write &dereference, makes no sense at ${addressOf.position}")
} else {
@@ -5877,11 +5952,21 @@ $endLabel""")
addressExpr is PtBinaryExpression -> {
val result = asmgen.pointerViaIndexRegisterPossible(addressExpr)
if(result!=null) {
val addressOfIdentifier = (result.first as? PtAddressOf)?.identifier
val addrOf = result.first as? PtAddressOf
val arrayId = addrOf?.identifier
val arrayDt = arrayId?.type
if(addrOf!=null && addrOf.isFromArrayElement && arrayDt?.sub == BaseDataType.STRUCT_INSTANCE && result.second is PtNumber) {
val offset = (result.second as PtNumber).number.toInt()
if(byteValue==null) asmgen.out(" pha")
computeStructArrayElementAddress(arrayDt, asmgen.asmSymbolName(arrayId), addrOf.arrayIndexExpr!!, "P8ZP_SCRATCH_PTR")
if(byteValue==null) asmgen.out(" pla") else asmgen.out(" lda #${byteValue.toHex()}")
asmgen.out(" ldy #$offset | sta (P8ZP_SCRATCH_PTR),y")
return
}
val addressOfIdentifier = addrOf?.identifier
if(addressOfIdentifier!=null) {
var varname = asmgen.asmVariableName(addressOfIdentifier)
if(addressOfIdentifier.type.isSplitWordArray(program.memsizer)) {
val addrOf = result.first as PtAddressOf
varname = if(addrOf.isMsbForSplitArray) varname+"_msb" else varname+"_lsb"
}
if(result.second is PtNumber) {
@@ -582,7 +582,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
private fun translate(arrayIx: PtArrayIndexer): ExpressionCodeResult {
if(arrayIx.type.isStructInstance)
throw AssemblyError("cannot translate POINTER[x] resulting in a struct instance; this is likely part of a larger expression POINTER[x].field and that has to be translated earlier as a whole")
throw AssemblyError("cannot translate array[x] resulting in a struct instance; this is likely part of a larger expression array[x].field and that has to be translated earlier as a whole")
val eltSize = codeGen.program.memsizer.memorySize(arrayIx.type, null)
val vmDt = codeGen.irType(arrayIx.type)
+63 -28
View File
@@ -863,36 +863,65 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
emitLine("dc.b ${bytesStr}0", v.name)
}
dt.isArray && init is IRVariableInitializer.Array -> {
val elemDt = dt.elementType()
val elemSize = if(elemDt.isByte || elemDt.isBool) 1 else if(elemDt.isLong) 4 else {
// on 32-bit targets, arrays of string pointers (stored as uword[]) need 4 bytes per element
if(target.POINTER_MEM_SIZE > 2u && init.elements.any { it is IRStSymbolicReference.Symbol } && elemDt.isWord) target.POINTER_MEM_SIZE.toInt()
else target.memorySize(elemDt.base)
}
val values = init.elements.map { elt ->
when(elt) {
is IRStSymbolicReference.Numeric -> elt.value.toInt().toString()
is IRStSymbolicReference.Symbol -> fixNameSymbols(elt.name)
is IRStSymbolicReference.BoolValue -> if(elt.value) "1" else "0"
if(dt.sub==BaseDataType.STRUCT_INSTANCE) {
emitLine(" ALIGN 2")
emitLine("$label:")
for(elt in init.elements) {
val sym = (elt as? IRStSymbolicReference.Symbol)?.name ?: error("expected struct instance symbol for ${v.name}")
val instanceName = sym.removePrefix("@")
val instance = program.st.lookup(instanceName) as? IRStStructInstance ?: error("struct instance $instanceName not found for ${v.name}")
for(fieldVal in instance.values) {
val valueStr = when(val ref = fieldVal.value) {
is IRStSymbolicReference.Numeric -> ref.value.toInt().toString()
is IRStSymbolicReference.Symbol -> fixNameSymbols(ref.name)
is IRStSymbolicReference.BoolValue -> if(ref.value) "1" else "0"
}
when(fieldVal.dt) {
BaseDataType.UBYTE, BaseDataType.BOOL -> emitLine("dc.b $valueStr")
BaseDataType.BYTE -> emitLine("dc.b $valueStr")
BaseDataType.UWORD, BaseDataType.WORD -> emitLine("dc.w $valueStr")
BaseDataType.LONG -> emitLine("dc.l $valueStr")
BaseDataType.FLOAT -> {
val bytes = target.getFloatAsmBytes(valueStr.toDouble())
emitLine("dc.b $bytes")
}
BaseDataType.POINTER -> emitLine("dc.l $valueStr")
else -> error("unsupported struct field type ${fieldVal.dt} for ${v.name}")
}
}
}
}
when (elemSize) {
1 -> {
emitLine(" ALIGN 2")
emitLine("$label:")
emitLine("dc.b ${values.joinToString(",")}", v.name)
} else {
val elemDt = dt.elementType()
val elemSize = if(elemDt.isByte || elemDt.isBool) 1 else if(elemDt.isLong) 4 else {
// on 32-bit targets, arrays of string pointers (stored as uword[]) need 4 bytes per element
if(target.POINTER_MEM_SIZE > 2u && init.elements.any { it is IRStSymbolicReference.Symbol } && elemDt.isWord) target.POINTER_MEM_SIZE.toInt()
else target.memorySize(elemDt.base)
}
2 -> {
emitLine(" ALIGN 2")
emitLine("$label:")
emitLine("dc.w ${values.joinToString(",")}", v.name)
val values = init.elements.map { elt ->
when(elt) {
is IRStSymbolicReference.Numeric -> elt.value.toInt().toString()
is IRStSymbolicReference.Symbol -> fixNameSymbols(elt.name)
is IRStSymbolicReference.BoolValue -> if(elt.value) "1" else "0"
}
}
4 -> {
emitLine(" ALIGN $elemSize")
emitLine("$label:")
emitLine("dc.l ${values.joinToString(",")}", v.name)
when (elemSize) {
1 -> {
emitLine(" ALIGN 2")
emitLine("$label:")
emitLine("dc.b ${values.joinToString(",")}", v.name)
}
2 -> {
emitLine(" ALIGN 2")
emitLine("$label:")
emitLine("dc.w ${values.joinToString(",")}", v.name)
}
4 -> {
emitLine(" ALIGN $elemSize")
emitLine("$label:")
emitLine("dc.l ${values.joinToString(",")}", v.name)
}
else -> error("expected array element size 1,2 or 4 for ${v.name}")
}
else -> error("expected array element size 1,2 or 4 for ${v.name}")
}
}
dt.isNumeric || dt.isBool -> {
@@ -1026,7 +1055,11 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
emitRaw(" ALIGN $alignment")
}
emitLabel(fixNameSymbols(v.name))
emitLine("ds.b $size")
val typeComment = if(v.dt.isArray) {
"array of ${v.length} ${v.dt.elementType()}"
} else
v.dt.toString()
emitLine("ds.b $size ; $typeComment")
}
}
@@ -1037,7 +1070,7 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
for (si in structs) {
emitRaw(" ALIGN 2")
emitLabel(fixNameSymbols(si.name))
emitLine("ds.b ${si.size}")
emitLine("ds.b ${si.size} ; struct ${si.structName}")
}
}
@@ -1071,12 +1104,14 @@ internal class AsmGen(val program: IRProgram, internal val target: ICompilationT
emitSlabs(normalSlabs)
// register file (always at the end of BSS variables)
emitRaw(" ; flat integer virtual register file:")
emitRaw(" ALIGN 4")
emitLabel(REGFILE_LABEL)
emitLine("ds.b ${regFileLayout.totalSize}")
// float register file
if (floatRegFileLayout.totalSize > 0) {
emitRaw(" ; flat float virtual register file:")
emitRaw(" ALIGN 4")
emitLabel(FLOAT_REGFILE_LABEL)
emitLine("ds.b ${floatRegFileLayout.totalSize}")
@@ -2505,6 +2505,10 @@ internal class AstChecker(private val program: Program,
errors.err("arrays of typed pointers are currently not supported, use untyped pointer for now at field '${field.name}'", struct.position)
return false
}
if(dt.isArray && dt.sub==BaseDataType.STRUCT_INSTANCE) {
errors.err("arrays of struct instances are currently not supported as struct fields at '${field.name}'", struct.position)
return false
}
return dt.isBasic
|| dt.base.isNumericOrBool
|| dt.isPointer
@@ -9,10 +9,7 @@ import prog8.ast.expressions.StaticStructInitializer
import prog8.ast.expressions.StringLiteral
import prog8.ast.statements.*
import prog8.ast.walk.IAstVisitor
import prog8.code.core.BuiltinFunctions
import prog8.code.core.ICompilationTarget
import prog8.code.core.IErrorReporter
import prog8.code.core.Position
import prog8.code.core.*
import prog8.code.target.VMTarget
/**
@@ -123,8 +120,12 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
errors.err("builtin function cannot be redefined", struct.position)
else if(struct.name in keywords)
errors.err("struct name cannot be a keyword", struct.position)
struct.fields.forEach { field ->
if(field.type.isArray && field.type.sub==BaseDataType.STRUCT_INSTANCE)
errors.err("arrays of struct instances are currently not supported as struct fields at '${field.name}'", struct.position)
}
super.visit(struct)
}
@@ -1450,30 +1450,50 @@ _after:
// works on an already-evaluated simple value.
// resolve the struct type of the base expression
val baseDt = base.inferType(program).getOrUndef()
val struct = if(baseDt.isPointer) baseDt.subType as? StructDecl else null
if(struct==null) {
errors.err("cannot assign through this expression, expected a pointer to a struct", base.position)
return noModifications
}
// use the target's natural pointer width for the hoisted address (uword on 6502, long on 32-bit targets)
val addressDt = if(program.target.POINTER_MEM_SIZE > 2u) DataType.LONG else DataType.UWORD
val tmpVar = VarDecl.createAuto(addressDt, base.position)
val tmpIdent = IdentifierReference(listOf(tmpVar.name), base.position)
// we only need the integer value of the pointer expression, so a redundant outer
// pointer typecast can be stripped to avoid pointless double casts.
val baseCast = base as? TypecastExpression
val baseValue: Expression = if(baseCast!=null && (baseCast.type.isPointer || baseCast.type.isUnsignedWord)
&& baseCast.expression.inferType(program).getOrUndef().isIntegerOrBool)
TypecastExpression(baseCast.expression.copy(), addressDt, false, base.position)
else
TypecastExpression(base.copy(), addressDt, false, base.position)
val tmpAssign = Assignment(
AssignTarget(tmpIdent.copy(), null, null, null, false, position=tmpVar.position),
val useTmpVar = baseDt.isPointer
val tmpVar = if(useTmpVar) VarDecl.createAuto(addressDt, base.position) else null
val tmpIdent = if(useTmpVar) IdentifierReference(listOf(tmpVar!!.name), base.position) else null
val struct: StructDecl?
val baseValue: Expression
when {
baseDt.isPointer -> {
struct = baseDt.subType as? StructDecl
if(struct==null) {
errors.err("cannot assign through this expression, expected a pointer to a struct", base.position)
return noModifications
}
// we only need the integer value of the pointer expression, so a redundant outer
// pointer typecast can be stripped to avoid pointless double casts.
val baseCast = base as? TypecastExpression
baseValue = if(baseCast!=null && (baseCast.type.isPointer || baseCast.type.isUnsignedWord)
&& baseCast.expression.inferType(program).getOrUndef().isIntegerOrBool)
TypecastExpression(baseCast.expression.copy(), addressDt, false, base.position)
else
TypecastExpression(base.copy(), addressDt, false, base.position)
}
baseDt.isStructInstance -> {
struct = baseDt.subType as? StructDecl
if(struct==null) {
errors.err("cannot assign through this expression, expected a struct instance", base.position)
return noModifications
}
// base is a struct instance lvalue in memory; take its address
baseValue = addressOfStructInstance(base, addressDt)
}
else -> {
errors.err("cannot assign through this expression, expected a pointer to a struct or a struct instance", base.position)
return noModifications
}
}
val tmpAssign = if(useTmpVar) Assignment(
AssignTarget(tmpIdent!!.copy(), null, null, null, false, position=tmpVar!!.position),
baseValue,
AssignmentOrigin.USERCODE, base.position)
AssignmentOrigin.USERCODE, base.position) else null
// build the address of the final field; intermediate pointer fields in the chain are loaded via peekw/peekl
var address: Expression = tmpIdent.copy()
var address: Expression = if(useTmpVar) tmpIdent!!.copy() else baseValue
var currentStruct: StructDecl = struct
var fieldDt: DataType? = null
for((index, field) in fields.withIndex()) {
@@ -1544,8 +1564,12 @@ _after:
val pokeCall = FunctionCallStatement(
IdentifierReference(listOf(funcName), assignment.position),
mutableListOf(address, value), false, assignment.position)
val replacement = AnonymousScope(mutableListOf(tmpVar, tmpAssign, pokeCall), target.position)
return listOf(AstReplaceNode(assignment, replacement, parent))
return if(useTmpVar) {
val replacement = AnonymousScope(mutableListOf(tmpVar!!, tmpAssign!!, pokeCall), target.position)
listOf(AstReplaceNode(assignment, replacement, parent))
} else {
listOf(AstReplaceNode(assignment, pokeCall, parent))
}
}
override fun after(assignment: Assignment, parent: Node): Iterable<AstModification> {
@@ -1610,6 +1634,19 @@ _after:
false, assignment.position)
return listOf(AstReplaceNode(assignment, memcopy, parent))
}
// points[1] = points[2] --> memcopy(&points[2], &points[1], sizeof(struct))
val targetIdx = assignment.target.arrayindexed
if(targetIdx!=null) {
val tIdxType = targetIdx.inferType(program)
if(tIdxType.isStructInstance && tIdxType == idxType) {
val source = AddressOf(sourceIdx.plainarrayvar!!.copy(), sourceIdx.indexer.copy(), null, false, true, assignment.position)
val target = AddressOf(targetIdx.plainarrayvar!!.copy(), targetIdx.indexer.copy(), null, false, true, assignment.position)
val memcopy = FunctionCallStatement(IdentifierReference(listOf("sys", "memcopy"), assignment.position),
mutableListOf(source, target, structSizeNum),
false, assignment.position)
return listOf(AstReplaceNode(assignment, memcopy, parent))
}
}
}
}
@@ -1728,4 +1765,19 @@ _after:
return noModifications
}
private fun addressOfStructInstance(base: Expression, addressDt: DataType): Expression {
val addr = when(base) {
is IdentifierReference -> AddressOf(base.copy(), null, null, false, true, base.position)
is ArrayIndexedExpression -> AddressOf(
base.plainarrayvar?.copy(), base.indexer.copy(), null, false, true, base.position)
is TypecastExpression -> return addressOfStructInstance(base.expression, addressDt)
else -> {
errors.err("cannot take address of struct instance base expression $base", base.position)
// return a dummy to keep desugaring moving; AstChecker will catch the error later
AddressOf(IdentifierReference(listOf("_dummy"), base.position), null, null, false, true, base.position)
}
}
return TypecastExpression(addr, addressDt, false, base.position)
}
}
@@ -116,7 +116,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
val eltDt = decl.datatype.elementType()
if(eltDt.isPointer)
TODO("convert array of pointers to split words array type ${decl.position}")
else if(options.compTarget.POINTER_MEM_SIZE == 2u)
else if((eltDt.isWord || eltDt.isUnsignedWord) && options.compTarget.POINTER_MEM_SIZE == 2u)
DataType.splitWordArrayFor(eltDt.base)
else
null
+297
View File
@@ -0,0 +1,297 @@
package prog8tests.compiler
import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
import prog8.code.core.BaseDataType
import prog8.code.target.C64Target
import prog8.code.target.Qemu68kTarget
import prog8.code.target.VMTarget
import prog8.intermediate.IRFileReader
import prog8tests.helpers.compileText
class TestArraysOfStructs: FunSpec({
test("declare array of struct - symbol table") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[4] points
sub start() { points[0].x = 1 }
}
"""
val result = compileText(VMTarget(), true, src, out, writeAssembly = true, assemble = false)!!
val dt = result.codegenSymboltable!!.allVariables.first { it.name.endsWith("points") }.dt
dt.base shouldBe BaseDataType.ARRAY
dt.sub shouldBe BaseDataType.STRUCT_INSTANCE
dt.subType!!.scopedNameString shouldContain "Point"
}
test("field read via variable index") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[4] points
sub start() {
uword v = points[1].y
}
}
"""
val result = compileText(VMTarget(), true, src, out, writeAssembly = true, assemble = false)!!
val dt = result.codegenSymboltable!!.allVariables.first { it.name.endsWith("points") }.dt
dt.sub shouldBe BaseDataType.STRUCT_INSTANCE
}
test("field write via variable index desugars to poke") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[4] points
sub start() {
points[2].x = 1234
}
}
"""
compileText(C64Target(), true, src, out, writeAssembly = true, assemble = false)!!
val asm = out.toFile().listFiles()!!.single { it.name.endsWith(".asm") }.readText()
asm shouldContain "poke"
}
test("whole struct assignment becomes memcopy") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[4] points
sub start() {
points[1] = points[2]
}
}
"""
compileText(VMTarget(), true, src, out, writeAssembly = true, assemble = true)!!
val ir = out.toFile().listFiles()!!.single { it.name.endsWith(".p8ir") }.readText()
ir shouldContain "call sys.memcopy"
}
test("iterate over struct array with for loop") {
val out = tempdir().toPath()
val src = """
main {
struct Big { uword a uword b uword c uword d uword e uword f }
Big[5] bigs
sub start() {
for i in 0 to 4 {
bigs[i].a = i
}
}
}
"""
compileText(VMTarget(), true, src, out, writeAssembly = true, assemble = true)!!
val ir = out.toFile().listFiles()!!.single { it.name.endsWith(".p8ir") }.readText()
ir.split("bigs").size shouldBe 3 // declaration + loop read + loop write
}
test("initialized array of structs") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[3] arr = [^^Point : [1,2], ^^Point : [3,4], ^^Point : [5,6]]
sub start() {
uword v = arr[1].x
}
}
"""
compileText(VMTarget(), true, src, out, writeAssembly = true, assemble = true)!!
val ir = out.toFile().listFiles()!!.single { it.name.endsWith(".p8ir") }.readText()
ir shouldContain "<STRUCTINSTANCES>"
ir shouldContain "arr"
}
test("IR round-trip preserves struct field definitions") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[3] arr
sub start() {
arr[1].x = 1234
}
}
"""
compileText(VMTarget(), true, src, out, writeAssembly = true, assemble = true)!!
val irFile = out.toFile().listFiles()!!.single { it.name.endsWith(".p8ir") }.toPath()
val ir = IRFileReader().read(irFile)
val pointDef = ir.st.lookup("main.Point") as prog8.intermediate.IRStStructDef
pointDef.fields.size shouldBe 2
pointDef.fields.any { it.name == "x" } shouldBe true
pointDef.fields.any { it.name == "y" } shouldBe true
}
test("6502 stride >256 uses 16-bit offset") {
val out = tempdir().toPath()
val src = """
main {
struct Big130 {
uword w1
uword w2
uword w3
uword w4
uword w5
uword w6
uword w7
uword w8
uword w9
uword w10
uword w11
uword w12
uword w13
uword w14
uword w15
uword w16
uword w17
uword w18
uword w19
uword w20
uword w21
uword w22
uword w23
uword w24
uword w25
uword w26
uword w27
uword w28
uword w29
uword w30
uword w31
uword w32
uword w33
uword w34
uword w35
uword w36
uword w37
uword w38
uword w39
uword w40
uword w41
uword w42
uword w43
uword w44
uword w45
uword w46
uword w47
uword w48
uword w49
uword w50
uword w51
uword w52
uword w53
uword w54
uword w55
uword w56
uword w57
uword w58
uword w59
uword w60
uword w61
uword w62
uword w63
uword w64
uword w65
}
Big130[2] arr
sub start() {
arr[1].w65 = 1234
}
}
"""
compileText(C64Target(), true, src, out, writeAssembly = true, assemble = false)!!
val asm = out.toFile().listFiles()!!.single { it.name.endsWith(".asm") }.readText()
// field offset 128 on top of element 1 at 130 -> >255, must use 16-bit add
asm shouldContain "#>(p8b_main.p8v_arr + 130)"
asm shouldContain "adc #\$80"
}
test("6502 variable index stride >256 uses 16-bit scaling") {
val out = tempdir().toPath()
val src = """
main {
struct Big { uword a uword b uword c uword d uword e uword f uword g }
Big[50] arr
sub start() {
ubyte @shared idx
idx = 40
arr[idx].g = 1234
}
}
"""
compileText(C64Target(), true, src, out, writeAssembly = true, assemble = false)!!
val asm = out.toFile().listFiles()!!.single { it.name.endsWith(".asm") }.readText()
// size 14, idx 40 -> offset 560 >= 256; must use rol/16-bit multiply
(asm.contains("rol P8ZP_SCRATCH_W1+1") || asm.contains("multiply_words") || asm.contains("mul_word_14")) shouldBe true
}
test("6502 byte field at offset 0 with variable index") {
val out = tempdir().toPath()
val src = """
main {
struct Trip { ubyte a ubyte b ubyte c }
Trip[100] trips
sub start() {
ubyte @shared idx
idx = 50
trips[idx].a = 123
}
}
"""
compileText(C64Target(), true, src, out, writeAssembly = true, assemble = false)!!
val asm = out.toFile().listFiles()!!.single { it.name.endsWith(".asm") }.readText()
// must use indexed store, not a direct absolute address
asm shouldContain "(P8ZP_SCRATCH_PTR),y"
asm shouldNotContain "sta p8b_main.p8v_trips"
}
test("6502 byte field at non-zero offset with variable index") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x ubyte y }
Point[100] points
sub start() {
ubyte @shared idx
idx = 50
points[idx].y = 123
}
}
"""
compileText(C64Target(), true, src, out, writeAssembly = true, assemble = false)!!
val asm = out.toFile().listFiles()!!.single { it.name.endsWith(".asm") }.readText()
// must use indexed store with offset 2, not a direct absolute address
asm shouldContain "(P8ZP_SCRATCH_PTR),y"
asm shouldNotContain "sta p8b_main.p8v_points+2"
}
test("m68k struct array without vasm - IR check") {
val out = tempdir().toPath()
val src = """
main {
struct Point { uword x uword y }
Point[4] points
sub start() {
points[2].x = 1234
points[1] = points[2]
}
}
"""
val result = compileText(Qemu68kTarget(), true, src, out, writeAssembly = true, assemble = false)!!
result.codegenSymboltable!!.allVariables.any { it.name.endsWith("points") } shouldBe true
// check assembly was generated without needing vasm (assemble=false)
val asmFiles = out.toFile().listFiles()!!.filter { it.name.endsWith(".asm") }
(asmFiles.isNotEmpty() || result.codegenAst != null) shouldBe true
}
})
@@ -237,7 +237,7 @@ class Antlr2KotlinVisitor(val source: SourceCode, private val target: ICompilati
if(baseDt.isPointer) {
DataType.arrayOfPointersFromAntlrTo(baseDt.sub, baseDt.subTypeFromAntlr)
} else if(baseDt.isStructInstance)
throw SyntaxError("array of structures not allowed (use array of pointers)", ctx.toPosition())
DataType.arrayOfStructsFromAntlr(baseDt.subTypeFromAntlr ?: emptyList())
else
DataType.arrayFor(baseDt.base, target) // str arrays become LONG[] on 32-bit targets via pointerBaseType
}
@@ -838,6 +838,15 @@ class Antlr2KotlinVisitor(val source: SourceCode, private val target: ICompilati
return AssignTarget(null, null, null, null, false, dotExpression = expr, position = ctx.toPosition())
}
// array[index].field as assignment target; the CodeDesugarer turns this into a poke-style write.
override fun visitArrayindexedDerefTarget(ctx: ArrayindexedDerefTargetContext): AssignTarget {
var expr: Expression = ctx.arrayindexed().accept(this) as ArrayIndexedExpression
for(field in ctx.identifier()) {
expr = BinaryExpression(expr, ".", IdentifierReference(listOf(field.text), ctx.toPosition()), ctx.toPosition())
}
return AssignTarget(null, null, null, null, false, dotExpression = expr, position = ctx.toPosition())
}
/**
* Visits a pointer dereference expression.
*
@@ -1289,10 +1289,13 @@ class ArrayLiteral(val type: InferredTypes.InferredType, // inferred because
val unique = dts.toSet()
if(unique.size==1) {
val dt = unique.single()
return if(dt.subType!=null)
InferredTypes.knownFor(DataType.arrayOfPointersTo(dt.subType!!))
else
InferredTypes.knownFor(DataType.arrayOfPointersTo(dt.sub!!))
if(dt.subType!=null) {
val parentDecl = parent as? VarDecl
if(parentDecl!=null && parentDecl.datatype.base==BaseDataType.ARRAY_POINTER)
return InferredTypes.knownFor(DataType.arrayOfPointersTo(dt.subType!!))
return InferredTypes.knownFor(DataType.arrayOfStructs(dt.subType!!))
} else
return InferredTypes.knownFor(DataType.arrayOfPointersTo(dt.sub!!))
}
}
return when {
+58 -7
View File
@@ -151,7 +151,7 @@ dealing with all of them separately. You first define the struct type like so::
Allowed field datatypes: all numeric types (``byte``, ``ubyte``, ``word``, ``uword``, ``long``, ``float``),
``bool``, ``str``, typed pointers to those or to other structs, and inline 1D arrays of these element types.
Not allowed: struct instances themselves (structs cannot be nested, use a pointer to the struct instead),
arrays of typed pointers such as ``^^Enemy[4]`` (use untyped pointers instead), and 2D arrays.
arrays of struct instances, arrays of typed pointers such as ``^^Enemy[4]`` (use untyped pointers instead), and 2D arrays.
The typed pointer array and 2D array restrictions are limitations of the current compiler; they may or may not be lifted in a future version.
Fields in a struct are 'packed' (meaning the values are placed back-to-back in memory), and placed in memory in order of declaration. This guarantees exact size and place of the fields.
@@ -187,19 +187,70 @@ The compiler evaluates the pointer expression exactly once and then stores the v
.. note::
Structs are currently only supported as a *reference type* (they always have to be accessed through a pointer).
It is not yet possible to use them as a value type, or as memory-mapped types.
This means you cannot create an array of structs either - only arrays of pointers to structs.
There are a couple of simple case where the compiler does allow assignment of struct instances though, and it will
automatically copy all the fields for you. You are allowed to write::
Structs are primarily supported as a *reference type* accessed through a pointer.
However, you can also declare an *array of struct instances* (see below), and a plain struct instance
can be used as an lvalue in a few specific assignment cases.
It is not yet possible to use a struct as a standalone value type, or as a memory-mapped type.
The compiler replaces whole-struct assignments with a memory copy. You are allowed to write::
ptr2^^ = ptr1^^
ptr2^^ = ptr1[2]
ptr2[2] = ptr1^^
arr[i] = arr[j] ; array of struct instances
The compiler replaces this with a memory copy if these are pointers to a struct.
In the future more cases may be supported.
.. _structarrays:
Arrays of struct instances
--------------------------
.. index:: pair: Structs; Arrays of instances
You can declare an array that contains struct instances directly, rather than pointers to structs.
The syntax places the array size between the struct type and the variable name::
struct Enemy {
ubyte xpos, ypos
uword health
bool elite
}
Enemy[10] enemies ; 10 Enemy instances in a contiguous block
Indexing works as expected and the fields are accessed with the dotted notation::
enemies[i].health = 100
enemies[i].elite = true
ubyte x = enemies[i].xpos
You can copy one whole element to another with a single assignment; the compiler generates a memory copy::
enemies[i] = enemies[j]
Array-of-struct variables can also be statically initialized by nesting struct initializers inside an array literal::
Enemy[3] squad = [
^^Enemy : [10, 20, 100, false],
^^Enemy : [30, 40, 200, true],
^^Enemy : [50, 60, 150, false]
]
The struct size counts toward the same target size limits as other arrays.
On 6502-family targets a struct cannot exceed 256 bytes, so the maximum number of elements in an
array of structs is limited by that size (for example, at most 128 elements for a 2-byte struct, and
fewer for larger structs). On 32-bit targets the per-array size limit is much larger.
.. note::
Arrays of struct instances cannot be used as subroutine parameters (the same restriction applies
to most array types other than ``ubyte`` arrays and strings). Pass a typed pointer to the first
element instead, or use ``sys.memcopy`` if you need to pass a copy.
.. note::
Arrays of struct instances are stored contiguously, so accessing fields with a variable index
is less efficient than accessing fields through a typed pointer. For performance-critical code,
prefer typed pointers to structs unless the contiguous array layout is required.
.. note::
Using structs instead of plain arrays usually results in more and less efficient code being generated.
This is because the 6502 CPU is not particularly well equipped to dealing with pointers and accessing struct fields via offsets,
+1 -1
View File
@@ -6,7 +6,7 @@ Future Things and Ideas
- Consider extending ``var xx = <value>`` type inference (recently added for for-loop counter variables) to all variable declarations. Investigate pros/cons: desired syntax (``var x = expr`` vs ``var x := expr``), interaction with existing ``ubyte``/``uword``/``word`` inference defaults, const vs var, scope and shadowing, error messages for ambiguous types, documentation, impact on block/sub scope, and whether ``var`` without initializer should be allowed.
- DataType.ARRAY_POINTER depends on the compilation target to be either a split word array or not. This is horrible because now we have to check with the compilation target everywhere to see if a DataType enumeration value is split word array, and PtVariable and PtArrayIndexer need an explicit boolean to tell us if this is the case. See ideas/remove_array_pointer_plan.md for the plan.
- m68k codegen: make use of scaling factors in the indexed instructions on 68020+ ? see ideas/scaled-indexing-IR.md
- support arrays-of-structs, see ideas/arrays-of-structs.md
- support arrays-of-struct-instances as struct fields (e.g. ``struct Outer { Inner[4] inners }``). Currently rejected with an error.
- split up AssignmentAsmGen.kt in codeGenCpu6502 it is by far the largest file 6000+ lines
- make enums strongly typed instead of just syntactic sugar for ints (see ideas/enum-strong-type.md for the plan)
- extend the ``-gendoc`` command to generate user reference documentation from Markdown docstrings; see ``ideas/markdown-docstrings-and-reference-docs.md`` for the plan.
+32 -30
View File
@@ -268,36 +268,37 @@ Data Types
Prog8 supports the following data types:
=============== ======================= ================= =========================================
type identifier type storage size example var declaration and literal value
=============== ======================= ================= =========================================
``byte`` signed byte 1 byte = 8 bits ``byte myvar = -22``
``ubyte`` unsigned byte 1 byte = 8 bits ``ubyte myvar = $8f``, ``ubyte c = 'a'``
``bool`` boolean 1 byte = 8 bits ``bool myvar = true`` or ``bool myvar = false``
``word`` signed word 2 bytes = 16 bits ``word myvar = -12345``
``uword`` unsigned word 2 bytes = 16 bits ``uword myvar = $8fee``
``long`` signed 32 bits integer 4 bytes ``long large = $12345678``
there is no unsigned long type at the moment.
``float`` floating-point 5 bytes = 40 bits ``float myvar = 1.2345``
stored in 5-byte cbm MFLPT format
``byte[x]`` signed byte array x bytes ``byte[4] myvar``
``ubyte[x]`` unsigned byte array x bytes ``ubyte[4] myvar``
``word[x]`` signed word array 2*x bytes ``word[4] myvar``
``uword[x]`` unsigned word array 2*x bytes ``uword[4] myvar``
``float[x]`` floating-point array 5*x bytes ``float[4] myvar``. The 5 bytes per float is on CBM targets.
``bool[x]`` boolean array x bytes ``bool[4] myvar`` note: consider using bit flags in a byte or word instead to save space
``byte[]`` signed byte array depends on value ``byte[] myvar = [1, 2, 3, 4]``
``ubyte[]`` unsigned byte array depends on value ``ubyte[] myvar = [1, 2, 3, 4]``
``word[]`` signed word array depends on value ``word[] myvar = [1, 2, 3, 4]``
``uword[]`` unsigned word array depends on value ``uword[] myvar = [1, 2, 3, 4]``
``float[]`` floating-point array depends on value ``float[] myvar = [1.1, 2.2, 3.3, 4.4]``
``bool[]`` boolean array depends on value ``bool[] myvar = [true, false, true]`` note: consider using bit flags in a byte or word instead to save space
``str[]`` array with string ptrs 2*x bytes + strs ``str[] names = ["ally", "pete"]`` note: equivalent to a uword array.
``str`` string (PETSCII) varies ``str myvar = "hello."``
implicitly terminated by a 0-byte
``^^type`` typed pointer 2 or 4 bytes pointer types are explained in their own chapter :ref:`pointers`
``pointer`` untyped address 2 or 4 bytes ``pointer ptr = $a000``. See :ref:`pointers` for details.
=============== ======================= ================= =========================================
================== ======================= ====================== =========================================
type identifier type storage size example var declaration and literal value
================== ======================= ====================== =========================================
``byte`` signed byte 1 byte = 8 bits ``byte myvar = -22``
``ubyte`` unsigned byte 1 byte = 8 bits ``ubyte myvar = $8f``, ``ubyte c = 'a'``
``bool`` boolean 1 byte = 8 bits ``bool myvar = true`` or ``bool myvar = false``
``word`` signed word 2 bytes = 16 bits ``word myvar = -12345``
``uword`` unsigned word 2 bytes = 16 bits ``uword myvar = $8fee``
``long`` signed 32 bits integer 4 bytes ``long large = $12345678``
there is no unsigned long type at the moment.
``float`` floating-point 5 bytes = 40 bits ``float myvar = 1.2345``
stored in 5-byte cbm MFLPT format
``byte[x]`` signed byte array x bytes ``byte[4] myvar``
``ubyte[x]`` unsigned byte array x bytes ``ubyte[4] myvar``
``word[x]`` signed word array 2*x bytes ``word[4] myvar``
``uword[x]`` unsigned word array 2*x bytes ``uword[4] myvar``
``float[x]`` floating-point array 5*x bytes ``float[4] myvar``. The 5 bytes per float is on CBM targets.
``bool[x]`` boolean array x bytes ``bool[4] myvar`` note: consider using bit flags in a byte or word instead to save space
``byte[]`` signed byte array depends on value ``byte[] myvar = [1, 2, 3, 4]``
``ubyte[]`` unsigned byte array depends on value ``ubyte[] myvar = [1, 2, 3, 4]``
``word[]`` signed word array depends on value ``word[] myvar = [1, 2, 3, 4]``
``uword[]`` unsigned word array depends on value ``uword[] myvar = [1, 2, 3, 4]``
``float[]`` floating-point array depends on value ``float[] myvar = [1.1, 2.2, 3.3, 4.4]``
``bool[]`` boolean array depends on value ``bool[] myvar = [true, false, true]`` note: consider using bit flags in a byte or word instead to save space
``str[]`` array with string ptrs 2*x bytes + strs ``str[] names = ["ally", "pete"]`` note: equivalent to a uword array.
``str`` string (PETSCII) varies ``str myvar = "hello."``
implicitly terminated by a 0-byte
``StructType[x]`` array of struct inst. x * sizeof(struct) ``Enemy[10] enemies`` see :ref:`structarrays`
``^^type`` typed pointer 2 or 4 bytes pointer types are explained in their own chapter :ref:`pointers`
``pointer`` untyped address 2 or 4 bytes ``pointer ptr = $a000``. See :ref:`pointers` for details.
================== ======================= ====================== =========================================
.. note::
The actual size of pointer types (``^^type`` and ``pointer``) depends on the target's
@@ -429,6 +430,7 @@ or when adding more stuff to the array later. Here are some examples of arrays::
uword[] others = [names, array] ; array of pointers/addresses to other arrays
bool[2] flags = [true, false] ; array of two boolean values (take up 1 byte each, like a byte array)
^^float[3] values ; array of three pointers to floats
Enemy[10] enemies ; array of 10 Enemy struct instances (see :ref:`pointers`)
uword[2] memarr = [memory("m1",10,0), memory("m2",20,0)] ; array of memory block addresses
value = array[3] ; the fourth value in the array (index is 0-based)
+35 -26
View File
@@ -1,39 +1,48 @@
%import custom
%import textio
%zeropage basicsafe
main {
/** struct one */
struct Foo {
bool z
struct Enemy {
ubyte xpos, ypos
uword health
bool elite
}
/** alias one */
alias QQ = Foo
/** thingymabob */
const ubyte thing = 33
/**
hello1
*/
sub start() {
^^QQ @shared wot = []
; Enemy[4] enemies ; 4 Enemy instances in a contiguous block, initialized to zero
; ^^Enemy[4] @nosplit enemyptrs ; 4 Enemy pointers
;
; for index in 0 to 3 {
; txt.print_uwhex(enemyptrs[index], true)
; txt.spc()
; }
;
; for index in 0 to 3 {
; txt.print_uw(enemies[index].health)
; txt.spc()
; }
;
; enemies[2] = enemies[3]
; sys.memcopy(&enemies[3], &enemies[2], sizeof(Enemy))
custom.grab_system()
/**
hello2
*/
bool pal = custom.isPAL
; TODO: this is not actually acceptable syntax:
; an array of 3 Enemy structs is initialized with the syntax for an Enemy *pointer* array
; I expect it to be a compilation error with ^^Enemy : [10, 20, 100, false]
; I expect it to work with Enemy : [10, 20, 100, false] (notice no ^^ pointer)
; I expect it to work with [10, 20, 100, false] (no explicit type, inferred from the array type by the compiler)
Enemy[3] squad = [
^^Enemy : [10, 20, 100, false],
^^Enemy : [30, 40, 200, true],
^^Enemy : [50, 60, 150, false]
]
; ntsc
bool ntsc = not pal
custom.restore_system()
txt.print("pal? ")
txt.print_bool(pal)
txt.nl()
txt.print_uw(squad[2].health)
txt.nl()
squad[2].health = 9977
txt.print_uw(squad[2].health)
txt.nl()
}
}
-142
View File
@@ -1,142 +0,0 @@
# Arrays of Structs — Analysis & Change Overview
Prog8 does not currently allow declaring an array whose elements are
structures. This document analyzes the current state of the code and outlines
the changes needed to support `structtype name[size]`.
## Current state
The feature is **explicitly gated** in the parser. Declaring an array of
structures throws a hard error:
- `compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt:239-240`
```kotlin
} else if(baseDt.isStructInstance)
throw SyntaxError("array of structures not allowed (use array of pointers)", ctx.toPosition())
```
The recommended workaround today is `structtype[size]*` — an array of pointers
to separately-allocated structs — because the pointer element size is fixed
(2 bytes on 6502, 4 on m68k) and needs no struct-size stride.
A second gate exists in `codeCore/src/prog8/code/core/DataTypes.kt:202-204`:
```kotlin
if(dt.isStructInstance)
TODO("cannot use struct instance as a data type (yet) - use a pointer instead")
```
This must also be relaxed for struct-typed array elements.
## What already exists (the hard parts are partly done)
A surprising amount of the supporting machinery is already present:
- **Array indexer accepts struct elements.** `PtArrayIndexer`
(`simpleAst/src/prog8/code/ast/AstExpressions.kt:235`) already permits
`elementType.isStructInstance`, so the AST node is ready.
- **Index × struct-size math exists.** `ExpressionGen.kt:167,183` computes
`offset = constIndex * struct.size` and `multiplyByConst(indexReg,
struct.size)` when resolving a struct field; `:1868-1888` handles
"index * structsize + field" for struct field access. The element-base
arithmetic for arrays of structs is essentially the same code.
- **Array iteration uses element size.** `IRCodeGen.kt:776` computes
`elementSize = program.memsizer.memorySize(arrElementDt, null)` and `:777` does
`lengthBytes = iterableLength * elementSize`. If `arrElementDt` is a struct,
this already yields `struct.size`.
So this is not a from-scratch feature: the gates are the parser throw plus
making "struct as array element type" legal everywhere, with consistent
stride handling.
## Change overview
### 1. Parser / declaration
- Remove (or relax) the `SyntaxError` at `Antlr2KotlinVisitor.kt:239-240` so a
struct `DataType` can flow into the array-type builder.
- Also relax the `TODO` at `DataTypes.kt:202-204` so a struct instance can be
used as a data type.
- Allow `structtype name[size]` (and `const` / initialized forms).
### 2. DataType model (`codeCore/.../DataTypes.kt`)
- Represent "array of structs" as a `DataType` (extend `arrayFor` to accept a
struct `DataType`, or add `arrayOfStructs`). Today `arrayFor` takes a
`BaseDataType`; a struct instance is a full `DataType`.
- Ensure `elementType()` returns the struct `DataType`.
- Ensure `memsize` of the array = `length * struct.size`.
### 3. Memory layout & sizing
- Variable declaration must size the array as `length * struct.size` and align
the base to the struct's alignment. The element-size plumbing already exists
(`IRCodeGen.kt:776-777`); once the element type is legal it computes the
right total.
- The `IRStStaticVariable` / static-var representation must carry the struct
element type so sizing and alignment are correct.
### 4. Indexing / element stride (the core change)
- Every array-index operation multiplies the index by the **element stride**.
That stride must become `struct.size` (possibly > 4 bytes), not the fixed
1/2/4 of a numeric base type.
- `LOADX` / `STOREX` and pointer arithmetic must use this stride. For structs
you typically compute the element base `base + index*struct.size` and then
access fields within it, rather than a single wide load. The field-access
machinery in `ExpressionGen.kt:167,183` and `:1858-1866` is the template to
reuse.
### 5. Member access `arr[i].field`
- Parser: allow `.field` after an array indexer whose element is a struct.
- Codegen: combine `i * struct.size + fieldOffset` into one address using the
existing struct-field offset logic.
### 6. Whole-struct operations
- Assignment `arr[i] = otherStruct` (and array element as LHS) → **memcopy of
`struct.size` bytes**, not a scalar move. Struct-to-struct copy already
exists elsewhere; it must handle the array-element LHS address
(`base + i*struct.size`).
- `PtArrayIndexer` already permits struct element types, so the AST node is
ready.
### 7. Initializers
- Array-of-struct literal: each element is a struct initializer; emit
`struct.size` bytes per element into `.data` / `.bss`, with correct
alignment.
### 8. VM & backends (6502 / m68k / new6502)
- VM array load/store and `memcopy` are byte-addressable, so `struct.size`-
stride copies work; verify `LOADX` / `STOREX` use the element byte size
(not assume a 1-byte stride).
- Backends: element address math uses stride = `struct.size`; on 6502 this
needs 16-bit offset handling (structs can exceed 256 bytes), on m68k 32-bit
is trivial. Pointer / `loadIndexToD0` paths must use the struct stride.
### 9. Tests
Testing strategy: prefer generated-code assertions over simulation unless execution
behavior is the only way to validate a case. M68k gates must not require the `vasm`
assembler to be installed; verify via IR inspection or the existing backend unit
harnesses that run without external toolchains.
- Type tests (`elementType` for array-of-struct): `gradle :compiler:test` on the
`codeCore`/`compilerAst` datatype unit tests after the `DataType` model changes.
- Codegen assertion tests (no execution): add Prog8 snippets that declare, index,
access `arr[i].field`, assign whole structs, iterate, and initialize arrays of
structs; assert on generated IR/assembly via the compiler test helpers
(parse assembly/IR into lines and check specific instructions/address math),
not on runtime output. Target `virtual` is fine for IR inspection.
- 6502 stride gate: add a codegen test where `struct.size > 256` so the index math
requires 16-bit offset handling; assert the generated 6502 code uses a wide
offset (no 8-bit wrap) rather than running it in a simulator.
- M68k gate: assert generated M68K IR/assembly uses stride = `struct.size` and that
element-base arithmetic is correct, using the backend unit tests that do not
invoke `vasm`.
- Run `gradle build` (or at least `gradle :compiler:test`) as the final gate to
ensure no regressions in the existing array/struct handling.
## Bottom line
The two hard blockers are the parser throw (`Antlr2KotlinVisitor.kt:239-240`)
and the `TODO` in `DataTypes.kt:202-204`. The datatype/sizing path already
multiplies by element size in the right places. The real work is: (a) make
struct a legal array element type end-to-end, (b) ensure the *stride* is
`struct.size` consistently in all index math (not just field access),
(c) whole-struct copy for element LHS/RHS, and (d) initializers. The existing
`index*struct.size` code in `ExpressionGen` is the foundation to build on.
@@ -60,6 +60,19 @@ class IRFileReader {
private lateinit var options: CompilationOptions
private fun resolveStructPlaceholders(st: IRSymbolTable) {
// Replace IRSubtypePlaceholder subtypes (struct names read from IR text) with resolved
// subtypes backed by the actual IRStStructDef, so memory sizing and field lookups work.
for (node in st.allVariables()) {
val dt = node.dt
if (dt.subType is IRSubtypePlaceholder) {
val def = st.lookup((dt.subType as IRSubtypePlaceholder).scopedNameString) as? IRStStructDef
if (def != null)
dt.subType = IRStructSubtype(def)
}
}
}
private fun parseProgram(reader: XMLEventReader): IRProgram {
require(reader.nextEvent().isStartDocument)
val start = reader.nextEvent().asStartElement()
@@ -78,6 +91,7 @@ class IRFileReader {
val variables = parseVariables(reader)
val structsWithoutInit = parseStructInstancesNoInit(reader)
val structs = parseStructInstances(reader)
val structDefs = parseStructDefs(reader)
val constants = parseConstants(reader)
val memorymapped = parseMemMapped(reader)
val slabs = parseSlabs(reader)
@@ -98,6 +112,9 @@ class IRFileReader {
slabs.forEach { st.add(it) }
structs.forEach { st.add(it) }
structsWithoutInit.forEach { st.add(it) }
structDefs.forEach { st.add(it) }
resolveStructPlaceholders(st)
val program = IRProgram(programName, st, options, options.compTarget)
program.addGlobalInits(initGlobals)
@@ -309,6 +326,45 @@ class IRFileReader {
return IRStStructInstance(name, structName, emptyList(), size)
}
private fun parseStructDefs(reader: XMLEventReader): List<IRStStructDef> {
skipText(reader)
if(!reader.hasNext()) return emptyList()
val peek = reader.peek()
if(!peek.isStartElement || peek.asStartElement().name.localPart != "STRUCTDEFS")
return emptyList()
return parseSection(reader, "STRUCTDEFS") { line ->
val parts = line.split(' ', limit = 3)
val name = parts[0]
require(parts[1].startsWith("size="))
val size = parts[1].drop(5).toUInt()
val fields = if(parts.size > 2 && parts[2].startsWith("fields=")) {
val fieldsStr = parts[2].drop(7)
if(fieldsStr.isEmpty()) emptyList()
else fieldsStr.split(';').map { fieldSpec ->
val spaceIdx = fieldSpec.lastIndexOf(' ')
require(spaceIdx > 0) { "invalid struct field spec: $fieldSpec" }
val typeStr = fieldSpec.substring(0, spaceIdx)
val fieldName = fieldSpec.substring(spaceIdx + 1)
val (type, arraySize) = parseStructFieldType(typeStr)
IRStStructField(type, fieldName, arraySize)
}
} else emptyList()
IRStStructDef(name, fields, size)
}
}
private fun parseStructFieldType(typeStr: String): Pair<DataType, Int?> {
val bracketIdx = typeStr.indexOf('[')
return if(bracketIdx < 0) {
parseDatatype(typeStr, false) to null
} else {
require(typeStr.endsWith(']')) { "invalid array type $typeStr" }
val baseType = typeStr.substring(0, bracketIdx)
val size = typeStr.substring(bracketIdx + 1, typeStr.length - 1).toInt()
parseDatatype(baseType, true) to size
}
}
private fun parseStructInstances(reader: XMLEventReader): List<IRStStructInstance> =
parseSection(reader, "STRUCTINSTANCES") { line ->
parseStructInstance(line)
@@ -619,7 +675,7 @@ class IRFileReader {
"float" -> DataType.arrayFor(BaseDataType.FLOAT, options.compTarget)
"long" -> DataType.arrayFor(BaseDataType.LONG, options.compTarget)
else -> {
throw IRParseException("invalid dt $type")
DataType.arrayOfStructs(IRSubtypePlaceholder(type))
}
}
} else {
@@ -285,6 +285,7 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
writeInitializedVariables()
writeStructInstancesNoInit()
writeStructInstances()
writeStructDefs()
writeConstants()
writeMemoryMappedVariables()
writeMemorySlabs()
@@ -466,6 +467,18 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
xml.writeCharacters("\n")
}
private fun writeStructDefs() {
val defs = irProgram.st.allStructDefs()
xml.writeStartElement("STRUCTDEFS")
xml.writeCharacters("\n")
for (def in defs) {
val fields = def.fields.joinToString(";") { "${it.type.irTypeString(it.arraySize?.toUInt())} ${it.name}" }
emitLine("${def.name} size=${def.size} fields=${fields}")
}
xml.writeEndElement()
xml.writeCharacters("\n")
}
private fun writeConstants() {
xml.writeStartElement("CONSTANTS")
xml.writeCharacters("\n")
@@ -672,6 +672,14 @@ internal class IRSubtypePlaceholder(override val scopedNameString: String, val s
}
internal class IRStructSubtype(val def: IRStStructDef): ISubType {
override val scopedNameString: String get() = def.name
override fun memsize(sizer: IMemSizer) = def.size.toInt()
override fun sameas(other: ISubType) = other is IRStructSubtype && other.def.name == def.name
override fun getFieldType(name: String): DataType? = def.fields.firstOrNull { it.name == name }?.type
}
class RegistersUsed(
// register num -> number of uses
val readRegs: Map<RegisterNum, Int>,
@@ -35,6 +35,7 @@ fun DataType.irTypeString(length: UInt?): String {
BaseDataType.LONG -> "long[$lengthStr]"
BaseDataType.BOOL -> "bool[$lengthStr]"
BaseDataType.FLOAT -> "float[$lengthStr]"
BaseDataType.STRUCT_INSTANCE -> if(subType!=null) "${subType!!.scopedNameString}[$lengthStr]" else "$subTypeFromAntlr[$lengthStr]"
else -> throw IllegalArgumentException("invalid sub type")
}
}
+1
View File
@@ -245,6 +245,7 @@ assign_target:
| pointerdereference #PointerDereferenceTarget
| '(' expression ')' ('.' identifier)+ #ParenDerefTarget
| functioncall ('.' identifier)+ #FunctioncallDerefTarget
| arrayindexed ('.' identifier)+ #ArrayindexedDerefTarget
| VOID #VoidTarget
;
+35 -1
View File
@@ -263,6 +263,13 @@ class VmProgramLoader {
memory.setSL(addr, 0)
addr += 4u
}
dt.sub == BaseDataType.STRUCT_INSTANCE -> {
val elemSize = dt.subType?.memsize(program.options.compTarget) ?: 0
repeat(elemSize) {
memory.setUB(addr, 0u)
addr++
}
}
else -> throw IRParseException("invalid dt")
}
}
@@ -455,7 +462,34 @@ class VmProgramLoader {
}
}
else -> throw IRParseException("invalid dt")
variable.dt.base==BaseDataType.ARRAY && variable.dt.sub==BaseDataType.STRUCT_INSTANCE -> {
for (elt in iElts) {
val sym = (elt as? IRStSymbolicReference.Symbol)?.name ?: throw IRParseException("expected struct instance symbol for struct array init")
val instanceName = sym.removePrefix("@")
val instance = program.st.lookup(instanceName) as? IRStStructInstance ?: throw IRParseException("struct instance $instanceName not found for array init")
for (fieldVal in instance.values) {
val value: Double = when(val ref = fieldVal.value) {
is IRStSymbolicReference.BoolValue -> if(ref.value) 1.0 else 0.0
is IRStSymbolicReference.Numeric -> ref.value
is IRStSymbolicReference.Symbol -> {
val s = SymbolNames.stripPrefixes(ref.name)
val a = symbolAddresses[s] ?: throw IRParseException("symbol $s not found")
a.toDouble()
}
}
when {
fieldVal.dt.isByteOrBool -> { memory.setUB(address, value.toInt().toUByte()); address++ }
fieldVal.dt.isWord -> { memory.setUW(address, value.toInt().toUShort()); address += 2u }
fieldVal.dt.isPointer -> { memory.setUL(address, value.toUInt()); address += program.options.compTarget.POINTER_MEM_SIZE }
fieldVal.dt==BaseDataType.LONG -> { memory.setSL(address, value.toInt()); address += 4u }
fieldVal.dt==BaseDataType.FLOAT -> { memory.setFloat(address, value); address += program.options.compTarget.FLOAT_MEM_SIZE }
else -> throw IRParseException("invalid dt for struct field")
}
}
}
}
else -> throw IRParseException("invalid dt ${variable.name} sub=${variable.dt.sub} length=${variable.length}")
}
}