diff --git a/codeCore/src/prog8/code/core/DataTypes.kt b/codeCore/src/prog8/code/core/DataTypes.kt index d135baf84..7d7210152 100644 --- a/codeCore/src/prog8/code/core/DataTypes.kt +++ b/codeCore/src/prog8/code/core/DataTypes.kt @@ -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?): 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): 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") } } diff --git a/codeCore/src/prog8/code/target/NormalMemSizer.kt b/codeCore/src/prog8/code/target/NormalMemSizer.kt index fa900cc62..4fc0f6c2f 100644 --- a/codeCore/src/prog8/code/target/NormalMemSizer.kt +++ b/codeCore/src/prog8/code/target/NormalMemSizer.kt @@ -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") } diff --git a/codeCore/test/prog8tests/codecore/TestDataType.kt b/codeCore/test/prog8tests/codecore/TestDataType.kt index b89f0df9e..79e3fa9ab 100644 --- a/codeCore/test/prog8tests/codecore/TestDataType.kt +++ b/codeCore/test/prog8tests/codecore/TestDataType.kt @@ -127,10 +127,10 @@ class TestDataType: FunSpec({ DataType.forDt(BaseDataType.FLOAT) shouldBe DataType.FLOAT } - test("DataType.forDt throws for struct instance") { - shouldThrow { - 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") { diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt index c643b9146..91a204297 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt @@ -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)) diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/ProgramAndVarsGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/ProgramAndVarsGen.kt index cca0a7ed0..6dab04384 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/ProgramAndVarsGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/ProgramAndVarsGen.kt @@ -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") } } diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt index 040bd891b..91324ffe3 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt @@ -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) { diff --git a/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt b/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt index 0adf16612..e26bb884e 100644 --- a/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt +++ b/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt @@ -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) diff --git a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt index dc4cc0c2c..c37455d20 100644 --- a/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt +++ b/codeGenM68k/src/prog8/codegen/m68k/AsmGen.kt @@ -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}") diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index f7c6fcddc..2b9ad97db 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -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 diff --git a/compiler/src/prog8/compiler/astprocessing/AstIdentifiersChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstIdentifiersChecker.kt index 2e8831a99..eaab12089 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstIdentifiersChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstIdentifiersChecker.kt @@ -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) } diff --git a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt index 2305d5a3f..44a9f7765 100644 --- a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt +++ b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt @@ -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 { @@ -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) + } } diff --git a/compiler/src/prog8/compiler/astprocessing/VariousCleanups.kt b/compiler/src/prog8/compiler/astprocessing/VariousCleanups.kt index c86a1c19e..6a4ffc367 100644 --- a/compiler/src/prog8/compiler/astprocessing/VariousCleanups.kt +++ b/compiler/src/prog8/compiler/astprocessing/VariousCleanups.kt @@ -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 diff --git a/compiler/test/TestArraysOfStructs.kt b/compiler/test/TestArraysOfStructs.kt new file mode 100644 index 000000000..47d6cd2c8 --- /dev/null +++ b/compiler/test/TestArraysOfStructs.kt @@ -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 "" + 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 + } +}) diff --git a/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt b/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt index 55d6423dc..b12a5f0b3 100644 --- a/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt +++ b/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt @@ -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. * diff --git a/compilerAst/src/prog8/ast/expressions/AstExpressions.kt b/compilerAst/src/prog8/ast/expressions/AstExpressions.kt index 2f0807c2a..bf8beb409 100644 --- a/compilerAst/src/prog8/ast/expressions/AstExpressions.kt +++ b/compilerAst/src/prog8/ast/expressions/AstExpressions.kt @@ -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 { diff --git a/docs/source/structpointers.rst b/docs/source/structpointers.rst index ca1ed4fcd..863292209 100644 --- a/docs/source/structpointers.rst +++ b/docs/source/structpointers.rst @@ -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, diff --git a/docs/source/todo.rst b/docs/source/todo.rst index f1d5e82d9..a73b61d73 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -6,7 +6,7 @@ Future Things and Ideas - Consider extending ``var xx = `` 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. diff --git a/docs/source/variables.rst b/docs/source/variables.rst index 0b80437d4..6abf773f9 100644 --- a/docs/source/variables.rst +++ b/docs/source/variables.rst @@ -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) diff --git a/examples/test.p8 b/examples/test.p8 index fff8cc22a..a3069cc08 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -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() } } diff --git a/ideas/arrays-of-structs.md b/ideas/arrays-of-structs.md deleted file mode 100644 index e247f93a1..000000000 --- a/ideas/arrays-of-structs.md +++ /dev/null @@ -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. diff --git a/intermediate/src/prog8/intermediate/IRFileReader.kt b/intermediate/src/prog8/intermediate/IRFileReader.kt index d7583b07b..89fd132af 100644 --- a/intermediate/src/prog8/intermediate/IRFileReader.kt +++ b/intermediate/src/prog8/intermediate/IRFileReader.kt @@ -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 { + 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 { + 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 = 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 { diff --git a/intermediate/src/prog8/intermediate/IRFileWriter.kt b/intermediate/src/prog8/intermediate/IRFileWriter.kt index 338aed117..6d6ae9028 100644 --- a/intermediate/src/prog8/intermediate/IRFileWriter.kt +++ b/intermediate/src/prog8/intermediate/IRFileWriter.kt @@ -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") diff --git a/intermediate/src/prog8/intermediate/IRProgram.kt b/intermediate/src/prog8/intermediate/IRProgram.kt index 5680c8f8c..128be1799 100644 --- a/intermediate/src/prog8/intermediate/IRProgram.kt +++ b/intermediate/src/prog8/intermediate/IRProgram.kt @@ -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, diff --git a/intermediate/src/prog8/intermediate/Utils.kt b/intermediate/src/prog8/intermediate/Utils.kt index 92a75350e..2d9159589 100644 --- a/intermediate/src/prog8/intermediate/Utils.kt +++ b/intermediate/src/prog8/intermediate/Utils.kt @@ -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") } } diff --git a/parser/src/main/antlr/Prog8ANTLR.g4 b/parser/src/main/antlr/Prog8ANTLR.g4 index 32a538ac8..2cc0ea3a9 100644 --- a/parser/src/main/antlr/Prog8ANTLR.g4 +++ b/parser/src/main/antlr/Prog8ANTLR.g4 @@ -245,6 +245,7 @@ assign_target: | pointerdereference #PointerDereferenceTarget | '(' expression ')' ('.' identifier)+ #ParenDerefTarget | functioncall ('.' identifier)+ #FunctioncallDerefTarget + | arrayindexed ('.' identifier)+ #ArrayindexedDerefTarget | VOID #VoidTarget ; diff --git a/virtualmachine/src/prog8/vm/VmProgramLoader.kt b/virtualmachine/src/prog8/vm/VmProgramLoader.kt index 2b2a45fa4..e55760ce6 100644 --- a/virtualmachine/src/prog8/vm/VmProgramLoader.kt +++ b/virtualmachine/src/prog8/vm/VmProgramLoader.kt @@ -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}") } }