From af14562e2b2c4cbc0677876fbbbe58beead48aaa Mon Sep 17 00:00:00 2001 From: Irmen de Jong Date: Fri, 11 Sep 2026 01:32:48 +0200 Subject: [PATCH] allow struct field array sizes to be constant expressions fixes #240 --- .../compiler/astprocessing/AstChecker.kt | 13 +++- .../astprocessing/SimplifiedAstMaker.kt | 11 ++- compiler/test/ast/TestAstChecks.kt | 67 +++++++++++++++++++ .../src/prog8/ast/AstToSourceTextConverter.kt | 9 ++- .../prog8/ast/antlr/Antlr2KotlinVisitor.kt | 8 +-- .../src/prog8/ast/statements/AstStatements.kt | 33 +++++++-- compilerAst/src/prog8/ast/walk/AstWalker.kt | 1 + compilerAst/src/prog8/ast/walk/IAstVisitor.kt | 1 + examples/test.p8 | 26 +++---- 9 files changed, 133 insertions(+), 36 deletions(-) diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index 59dfcb8a5..d7ebea1ce 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -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) { diff --git a/compiler/src/prog8/compiler/astprocessing/SimplifiedAstMaker.kt b/compiler/src/prog8/compiler/astprocessing/SimplifiedAstMaker.kt index c208ad6b2..e56e3c1f6 100644 --- a/compiler/src/prog8/compiler/astprocessing/SimplifiedAstMaker.kt +++ b/compiler/src/prog8/compiler/astprocessing/SimplifiedAstMaker.kt @@ -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 { diff --git a/compiler/test/ast/TestAstChecks.kt b/compiler/test/ast/TestAstChecks.kt index 6c225b345..2dd726b34 100644 --- a/compiler/test/ast/TestAstChecks.kt +++ b/compiler/test/ast/TestAstChecks.kt @@ -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 { diff --git a/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt b/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt index c5d27beea..6fec1c7df 100644 --- a/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt +++ b/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt @@ -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("}") } diff --git a/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt b/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt index 81ecec7fe..e3c1b4167 100644 --- a/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt +++ b/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt @@ -814,11 +814,11 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor 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, val arraySize: Int?) + private data class StructFieldDef(val type: DataType, val names: List, 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, 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) = 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) = 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, 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, 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()) } diff --git a/compilerAst/src/prog8/ast/walk/AstWalker.kt b/compilerAst/src/prog8/ast/walk/AstWalker.kt index b538039d9..8e33352bb 100644 --- a/compilerAst/src/prog8/ast/walk/AstWalker.kt +++ b/compilerAst/src/prog8/ast/walk/AstWalker.kt @@ -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) } diff --git a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt index d1b9a86e2..17c9858b1 100644 --- a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt +++ b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt @@ -45,6 +45,7 @@ interface IAstVisitor { } fun visit(struct: StructDecl) { + struct.fields.forEach { it.arraySize?.accept(this) } } fun visit(field: StructFieldRef) { diff --git a/examples/test.p8 b/examples/test.p8 index e5284d9d5..d5aa6f7e4 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -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 } }