allow struct field array sizes to be constant expressions

fixes #240
This commit is contained in:
Irmen de Jong committed 2026-09-11 01:38:18 +02:00
1 parent ccc26a0eb2
commit af14562e2b
9 files changed
+133 -36

No files matched your search

@@ -2142,7 +2142,7 @@ internal class AstChecker(private val program: Program,
val deref = arrayIndexedExpression.pointerderef!!
val target = deref.definingScope.lookup(deref.chain)
val arraysize: Int? = when(target) {
is StructFieldRef -> target.field.arraySize
is StructFieldRef -> target.field.constSize()
is VarDecl -> target.arraysize?.constIndex()
else -> null
}
@@ -2342,10 +2342,17 @@ internal class AstChecker(private val program: Program,
|| dt.isPointer
|| dt.isArray
}
struct.fields.forEach { field ->
if(!validFieldType(field))
errors.err("invalid struct field type at '${field.name}'", struct.position)
if(field.isArray) {
val size = field.constSize()
if(size==null)
errors.err("array length must be known at compile-time", field.arraySize?.position ?: struct.position)
else if(size<=0)
errors.err("array length must be positive", field.arraySize?.position ?: struct.position)
}
}
}
@@ -2747,7 +2754,7 @@ internal class AstChecker(private val program: Program,
val struct = initializer.structname.targetStructDecl()
if(struct!=null) {
val expectedFieldCount = struct.fields.size
val expectedFlattenedElements = struct.fields.sumOf { it.arraySize ?: 1 }
val expectedFlattenedElements = struct.fields.sumOf { it.constSize() ?: 1 }
val flattenedArgs = args.flattenArgs()
if (args.size != expectedFieldCount) {
@@ -528,9 +528,9 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
private fun transform(initializer: StaticStructInitializer): PtFunctionCall {
val targetStruct = initializer.structname.targetStructDecl()!!
val call = PtFunctionCall("prog8_lib_structalloc", true, true, arrayOf(DataType.pointer(targetStruct)), initializer.position)
val flattenedArgs = initializer.args.flattenArgs()
val expectedTotalElements = targetStruct.fields.sumOf { it.arraySize ?: 1 }
val expectedTotalElements = targetStruct.fields.sumOf { it.constSize() ?: 1 }
if (flattenedArgs.isNotEmpty() && flattenedArgs.size != expectedTotalElements) {
errors.err("invalid number of field values: expected $expectedTotalElements flattened elements for struct '${targetStruct.name}' but got ${flattenedArgs.size}", initializer.position)
}
@@ -817,7 +817,12 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
}
private fun transform(struct: StructDecl): PtStructDecl {
return PtStructDecl(struct.name, struct.fields.map { PtStructField(it.type, it.name, it.arraySize) }, struct.position)
return PtStructDecl(struct.name, struct.fields.map {
val size = it.constSize()
if(it.isArray && size==null)
errors.err("array length must be known at compile-time", it.arraySize?.position ?: struct.position)
PtStructField(it.type, it.name, size)
}, struct.position)
}
private fun transform(srcWhen: When): PtWhen {
+67
View File
@@ -695,6 +695,73 @@ main {
errors.printedErrors.any { it.contains("2D arrays are not allowed as struct fields") } shouldBe true
}
test("struct field array size can be const expression") {
val src = """
main {
const ubyte SIZE = 5
struct Node {
ubyte[SIZE] data
uword top
}
sub start() {
^^Node n = [ [1,2,3,4,5], 9999 ]
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors, false)
errors.errors shouldBe emptyList()
}
test("struct field array size can be folded constant expression") {
val src = """
main {
const ubyte BASE = 3
struct Node {
ubyte[BASE*2] data
}
sub start() {
^^Node n = [ [1,2,3,4,5,6] ]
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors, false)
errors.errors shouldBe emptyList()
}
test("struct field array size rejects non-constant expression") {
val src = """
main {
uword dyn
struct Node {
uword[dyn] data
}
sub start() {
dyn = 5
}
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
errors.errors.any { it.contains("array length must be known at compile-time") } shouldBe true
}
test("struct field array size rejects zero size") {
val src = """
main {
const ubyte SIZE = 0
struct Node {
ubyte[SIZE] data
}
sub start() { }
}
""".trimIndent()
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors) shouldBe null
errors.errors.any { it.contains("array length must be positive") } shouldBe true
}
test("variable declaration followed by statement on same line should give error") {
val src = """
main {
@@ -194,8 +194,13 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
output("private ")
outputln("struct ${struct.name} {")
for(member in struct.fields) {
val arraySuffix = if(member.isArray) "[${member.arraySize}]" else ""
outputlni( " ${member.type}$arraySuffix ${member.name}")
outputi(" ${member.type}")
if(member.isArray) {
output("[")
member.arraySize?.indexExpr?.accept(this)
output("]")
}
outputln(" ${member.name}")
}
outputlni("}")
}
@@ -814,11 +814,11 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
override fun visitStructdeclaration(ctx: StructdeclarationContext): StructDecl {
val name = getname(ctx.identifier())
val fieldDefs = ctx.structfielddecl().map { getStructField(it) }
val flattened = fieldDefs.flatMap { (dt, names, arrSize) -> names.map { StructField(dt, it, arrSize) }}
val flattened = fieldDefs.flatMap { (dt, names, arrSize) -> names.map { StructField(dt, it, arrSize?.copy()) }}
return StructDecl(name, flattened.toTypedArray(), ctx.PRIVATE() != null, ctx.toPosition())
}
private data class StructFieldDef(val type: DataType, val names: List<String>, val arraySize: Int?)
private data class StructFieldDef(val type: DataType, val names: List<String>, val arraySize: ArrayIndex?)
private fun getStructField(ctx: StructfielddeclContext): StructFieldDef {
val identifiers = ctx.identifierlist()?.identifier() ?: emptyList()
@@ -833,9 +833,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
throw SyntaxError("2D arrays are not allowed as struct fields", ctx.toPosition())
val dt = baseDt.elementToArray()
val arrayIndexExpr = arrayIndices[0].accept(this) as ArrayIndex
val size = (arrayIndexExpr.indexExpr as? NumericLiteral)?.number?.toInt()
?: throw SyntaxError("array field size must be a constant integer expression", ctx.toPosition())
dt to size
dt to arrayIndexExpr
} else {
baseDt to null
}
@@ -517,20 +517,37 @@ class StructDecl(override val name: String, val fields: Array<StructField>, val
override fun linkParents(parent: Node) {
this.parent = parent
fields.forEach { it.arraySize?.linkParents(this) }
}
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
override fun referencesIdentifier(nameInSource: List<String>) = false
override fun copy() = StructDecl(name, fields.clone(), isPrivate, position)
override fun replaceChildNode(node: Node, replacement: Node) {
require(replacement is ArrayIndex)
val field = fields.firstOrNull { it.arraySize === node }
?: throw FatalAstException("invalid replace in $this at ${node.position}: $node -> $replacement")
field.arraySize = replacement
replacement.linkParents(this)
}
override fun referencesIdentifier(nameInSource: List<String>) = fields.any { it.arraySize?.referencesIdentifier(nameInSource)==true }
override fun copy() = StructDecl(name, fields.map { it.copy() }.toTypedArray(), isPrivate, position)
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
override fun memsize(sizer: IMemSizer): Int = fields.sumOf { field ->
if(field.isArray)
sizer.memorySize(field.type, field.arraySize!!)
sizer.memorySize(field.type, field.constSize() ?: 1)
else
sizer.memorySize(field.type, 1)
}
override fun sameas(other: ISubType): Boolean = other is StructDecl && other.name==name && other.fields.contentEquals(fields)
override fun sameas(other: ISubType): Boolean {
if(other !is StructDecl || other.name!=name || other.fields.size!=fields.size)
return false
return fields.zip(other.fields).all { (a, b) ->
a.name==b.name && a.type==b.type && when {
a.arraySize==null && b.arraySize==null -> true
a.arraySize!=null && b.arraySize!=null -> a.arraySize!! isSameAs b.arraySize!!
else -> false
}
}
}
override fun getFieldType(name: String): DataType? = fields.firstOrNull { it.name==name }?.type
fun getField(name: String): StructField? = fields.firstOrNull { it.name==name }
@@ -540,7 +557,7 @@ class StructDecl(override val name: String, val fields: Array<StructField>, val
fields.fold(0) { offset, field ->
if (field.name == fieldname)
return offset.toUByte()
val numElements = if(field.isArray) field.arraySize!! else 1
val numElements = if(field.isArray) field.constSize() ?: 1 else 1
offset + sizer.memorySize(field.type, numElements)
}
return null
@@ -548,8 +565,10 @@ class StructDecl(override val name: String, val fields: Array<StructField>, val
}
data class StructField(val type: DataType, val name: String, val arraySize: Int? = null) {
data class StructField(val type: DataType, val name: String, var arraySize: ArrayIndex? = null) {
val isArray: Boolean get() = arraySize != null
fun constSize(): Int? = arraySize?.constIndex()
fun copy(): StructField = StructField(type, name, arraySize?.copy())
}
@@ -380,6 +380,7 @@ abstract class AstWalker {
fun visit(struct: StructDecl, parent: Node) {
track(before(struct, parent), struct, parent)
struct.fields.forEach { it.arraySize?.accept(this) }
track(after(struct, parent), struct, parent)
}
@@ -45,6 +45,7 @@ interface IAstVisitor {
}
fun visit(struct: StructDecl) {
struct.fields.forEach { it.arraySize?.accept(this) }
}
fun visit(field: StructFieldRef) {
+10 -16
View File
@@ -1,20 +1,14 @@
main {
const uword STACK_SIZE = 10
struct Stack {
uword[STACK_SIZE] stack1
uword[20] stack2
uword top
}
sub start() {
cx16.r1 = private.foo()
cx16.r2 = mkword.foo()
}
}
private {
sub foo() -> uword {
cx16.r0++
return cx16.r0
}
}
mkword {
sub foo() -> uword {
cx16.r0++
return cx16.r0
^^Stack st = []
st.stack1[5] = 9999
st.stack2[5] = 8888
}
}