mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
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:
1 parent
b0d50c2a68
commit
94822b8662
26 files changed
+887
-293
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,7 @@ assign_target:
|
||||
| pointerdereference #PointerDereferenceTarget
|
||||
| '(' expression ')' ('.' identifier)+ #ParenDerefTarget
|
||||
| functioncall ('.' identifier)+ #FunctioncallDerefTarget
|
||||
| arrayindexed ('.' identifier)+ #ArrayindexedDerefTarget
|
||||
| VOID #VoidTarget
|
||||
;
|
||||
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user