fix some struct type and symbol lookup errors

This commit is contained in:
Irmen de Jong
2025-05-10 23:09:59 +02:00
parent 0b789b5f0b
commit 59c378089e
16 changed files with 271 additions and 69 deletions

View File

@@ -128,7 +128,11 @@ class DataType private constructor(val base: BaseDataType, val sub: BaseDataType
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null) BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null)
) )
fun forDt(dt: BaseDataType) = simpletypes.getValue(dt) fun forDt(dt: BaseDataType): DataType {
if(dt.isStructInstance)
TODO("cannot use struct instance as a data type (yet) - use a pointer instead")
return simpletypes.getValue(dt)
}
fun arrayFor(elementDt: BaseDataType, splitwordarray: Boolean=true): DataType { fun arrayFor(elementDt: BaseDataType, splitwordarray: Boolean=true): DataType {
require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" } require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" }
@@ -224,7 +228,7 @@ class DataType private constructor(val base: BaseDataType, val sub: BaseDataType
if(sub!=null) "${sub.name.lowercase()}[]" else if (subType!=null) "${subType!!.scopedNameString}[]" else "${subTypeFromAntlr}[]" if(sub!=null) "${sub.name.lowercase()}[]" else if (subType!=null) "${subType!!.scopedNameString}[]" else "${subTypeFromAntlr}[]"
} }
BaseDataType.STRUCT_INSTANCE -> { BaseDataType.STRUCT_INSTANCE -> {
if(sub!=null) sub.name.lowercase() else if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr" sub?.name?.lowercase() ?: if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr"
} }
else -> base.name.lowercase() else -> base.name.lowercase()
} }

View File

@@ -989,6 +989,9 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
else else
ExpressionCodeResult(result, returnRegSpec!!.dt, finalReturnRegister, -1) ExpressionCodeResult(result, returnRegSpec!!.dt, finalReturnRegister, -1)
} }
is StStruct -> {
throw AssemblyError("stray struct constructor should have been removed (normally it can only occur as initialization expression for a pointer variable)")
}
else -> { else -> {
if(callTarget.type == StNodeType.LABEL) { if(callTarget.type == StNodeType.LABEL) {
require(fcall.void) require(fcall.void)
@@ -998,7 +1001,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1) return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
} }
else { else {
throw AssemblyError("invalid node type") throw AssemblyError("invalid node type ${callTarget.type} at ${callTarget.astNode?.position}")
} }
} }
} }

View File

@@ -1889,7 +1889,7 @@ class IRCodeGen(
} }
} }
} }
is PtStructDecl -> { /* do nothing */ } is PtStructDecl -> { /* do nothing, should be found in the symbol table */ }
else -> TODO("weird block child node $child") else -> TODO("weird block child node $child")
} }
} }

View File

@@ -27,6 +27,9 @@ class IRUnusedCodeRemover(
// we could clean up the SymbolTable as well, but ONLY if these symbols aren't referenced somewhere still in an instruction or variable initializer value // we could clean up the SymbolTable as well, but ONLY if these symbols aren't referenced somewhere still in an instruction or variable initializer value
val prefix = "$blockLabel." val prefix = "$blockLabel."
val blockVars = irprog.st.allVariables().filter { it.name.startsWith(prefix) } val blockVars = irprog.st.allVariables().filter { it.name.startsWith(prefix) }
// check if there are symbols referenced elsewhere that we should not prune (even though the rest of the block is empty)
blockVars.forEach { stVar -> blockVars.forEach { stVar ->
irprog.allSubs().flatMap { it.chunks }.forEach { chunk -> irprog.allSubs().flatMap { it.chunks }.forEach { chunk ->
chunk.instructions.forEach { ins -> chunk.instructions.forEach { ins ->
@@ -47,6 +50,19 @@ class IRUnusedCodeRemover(
} }
} }
val blockStructs = irprog.st.allStructDefs().filter { it.name.startsWith(prefix) }
blockStructs.forEach { struct ->
irprog.st.allStructInstances().forEach { instance ->
if(instance.structName == struct.name)
return // a struct instance is declared using this struct type
}
irprog.st.allVariables().forEach { variable ->
if(variable.dt.isPointer || variable.dt.isStructInstance)
if(struct.name == variable.dt.subType!!.scopedNameString)
return // a variable exists with the struct as (pointer) type
}
}
irprog.st.removeTree(blockLabel) irprog.st.removeTree(blockLabel)
} }

View File

@@ -16,8 +16,8 @@ fun convertStToIRSt(sourceSt: SymbolTable?): IRSymbolTable {
StNodeType.MEMORYSLAB -> st.add(convert(it.value as StMemorySlab)) StNodeType.MEMORYSLAB -> st.add(convert(it.value as StMemorySlab))
StNodeType.STRUCTINSTANCE -> { StNodeType.STRUCTINSTANCE -> {
val instance = it.value as StStructInstance val instance = it.value as StStructInstance
val struct = st.lookup(instance.structName) as IRStStructDef val struct = sourceSt.lookup(instance.structName) as StStruct
st.add(convert(instance, struct)) st.add(convert(instance, struct.fields))
} }
StNodeType.STRUCT -> st.add(convert(it.value as StStruct)) StNodeType.STRUCT -> st.add(convert(it.value as StStruct))
else -> { } else -> { }
@@ -137,8 +137,8 @@ private fun convert(variable: StMemorySlab): IRStMemorySlab {
} }
private fun convert(instance: StStructInstance, struct: IRStStructDef): IRStStructInstance { private fun convert(instance: StStructInstance, fields: List<Pair<DataType, String>>): IRStStructInstance {
val values = struct.fields.zip(instance.initialValues).map { (field, value) -> val values = fields.zip(instance.initialValues).map { (field, value) ->
val elt = convertArrayElt(value) val elt = convertArrayElt(value)
IRStructInitValue(field.first.base, elt) IRStructInitValue(field.first.base, elt)
} }

View File

@@ -181,14 +181,20 @@ class UnusedCodeRemover(private val program: Program,
val declIndex = (parent as IStatementContainer).statements.indexOf(decl) val declIndex = (parent as IStatementContainer).statements.indexOf(decl)
val singleUseIndex = (parent as IStatementContainer).statements.indexOf(singleUse.parent) val singleUseIndex = (parent as IStatementContainer).statements.indexOf(singleUse.parent)
if(declIndex==singleUseIndex-1) { if(declIndex==singleUseIndex-1) {
if("ignore_unused" !in decl.definingBlock.options()) val callStruct = (assignment.value as IFunctionCall).target.targetStructDecl()
errors.info("replaced unused variable '${decl.name}' with void call, maybe this can be removed altogether", decl.position) if(callStruct!=null) {
val fcall = assignment.value as IFunctionCall // don't turn a struct instance allocation call to a void call, instead, remove everything
val voidCall = FunctionCallStatement(fcall.target, fcall.args, true, fcall.position) return listOf(IAstModification.Remove(assignment, assignment.parent as IStatementContainer))
return listOf( } else {
IAstModification.ReplaceNode(decl, voidCall, parent), if("ignore_unused" !in decl.definingBlock.options())
IAstModification.Remove(assignment, assignment.parent as IStatementContainer) errors.info("replaced unused variable '${decl.name}' with void call, maybe this can be removed altogether", decl.position)
) val fcall = assignment.value as IFunctionCall
val voidCall = FunctionCallStatement(fcall.target, fcall.args, true, fcall.position)
return listOf(
IAstModification.ReplaceNode(decl, voidCall, parent),
IAstModification.Remove(assignment, assignment.parent as IStatementContainer)
)
}
} }
} }
} else { } else {

View File

@@ -569,11 +569,17 @@ internal class AstChecker(private val program: Program,
if (p.type.isPointer) { if (p.type.isPointer) {
if (p.type.subType == null && p.type.subTypeFromAntlr!=null) errors.err("cannot find struct type ${p.type.subTypeFromAntlr?.joinToString(".")}", p.position) if (p.type.subType == null && p.type.subTypeFromAntlr!=null) errors.err("cannot find struct type ${p.type.subTypeFromAntlr?.joinToString(".")}", p.position)
} }
if(p.type.isStructInstance)
errors.err("structs can only be passed via a pointer", p.position)
} }
for((index, r) in subroutine.returntypes.withIndex()) { for((index, r) in subroutine.returntypes.withIndex()) {
if(r.isPointer && r.subType==null && r.subTypeFromAntlr!=null) if(r.isPointer && r.subType==null && r.subTypeFromAntlr!=null)
err("return type #${index+1}: cannot find struct type ${r.subTypeFromAntlr?.joinToString(".")}") err("return type #${index+1}: cannot find struct type ${r.subTypeFromAntlr?.joinToString(".")}")
if(r.isStructInstance)
err("structs can only be returned via a pointer")
} }
} }
@@ -1603,6 +1609,10 @@ internal class AstChecker(private val program: Program,
errors.err("function doesn't return a value", functionCallExpr.position) errors.err("function doesn't return a value", functionCallExpr.position)
} }
} }
else if(targetStatement is StructDecl) {
if(functionCallExpr.parent is IStatementContainer)
errors.err("static struct instance allocation can only occur as an initializer for a pointer variable", functionCallExpr.position)
}
if(builtinFunctionName in listOf("peek", "peekw")) { if(builtinFunctionName in listOf("peek", "peekw")) {
val pointervar = functionCallExpr.args[0] as? IdentifierReference val pointervar = functionCallExpr.args[0] as? IdentifierReference
@@ -1632,6 +1642,10 @@ internal class AstChecker(private val program: Program,
checkFunctionCall(targetStatement, functionCallStatement.args, functionCallStatement.position) checkFunctionCall(targetStatement, functionCallStatement.args, functionCallStatement.position)
checkUnusedReturnValues(functionCallStatement, targetStatement, errors) checkUnusedReturnValues(functionCallStatement, targetStatement, errors)
if(targetStatement is StructDecl) {
errors.err("static struct instance allocation can only occur as an initializer for a pointer variable", functionCallStatement.position)
}
if(functionCallStatement.void) { if(functionCallStatement.void) {
when(targetStatement) { when(targetStatement) {
is BuiltinFunctionPlaceholder -> { is BuiltinFunctionPlaceholder -> {
@@ -1808,11 +1822,14 @@ internal class AstChecker(private val program: Program,
} }
} }
} }
// TODO rest
// TODO rest?
} }
args.forEach{ args.forEach{
checkLongType(it) checkLongType(it)
if(it.inferType(program).isStructInstance)
errors.err("structs can only be passed via a pointer", it.position)
} }
} }
@@ -2019,9 +2036,9 @@ internal class AstChecker(private val program: Program,
errors.err("unable to determine type of dereferenced pointer expression", deref.position) errors.err("unable to determine type of dereferenced pointer expression", deref.position)
} }
override fun visit(deref: PtrIndexedDereference) { override fun visit(idxderef: PtrIndexedDereference) {
if(compilerOptions.compTarget.name != VMTarget.NAME) if(compilerOptions.compTarget.name != VMTarget.NAME)
TODO("typed pointers are not yet supported in the 6502 compilation targets (only virtual) ${deref.position}") TODO("typed pointers are not yet supported in the 6502 compilation targets (only virtual) ${idxderef.position}")
// TODO ast checks for this one? // TODO ast checks for this one?
} }

View File

@@ -398,11 +398,11 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
val (target, _) = srcCall.target.targetNameAndType(program) val (target, _) = srcCall.target.targetNameAndType(program)
val iType = srcCall.inferType(program) val iType = srcCall.inferType(program)
val subtypeStruct = iType.getOrUndef().subType
val call = val call =
if(iType.isStructInstance) { if(subtypeStruct!=null) {
// a call to a struct yields a struct instance and means: allocate a statically initialized struct instance of that type // a call to a struct yields a pointer to a struct instance and means: allocate a statically initialized struct instance of that type
val struct = iType.getOrUndef().subType!! val pointertype = DataType.pointerToType(subtypeStruct)
val pointertype = DataType.pointerToType(struct)
PtBuiltinFunctionCall("structalloc", false, true, pointertype, srcCall.position) PtBuiltinFunctionCall("structalloc", false, true, pointertype, srcCall.position)
} else { } else {
// regular function call // regular function call

View File

@@ -292,7 +292,7 @@ class TypecastsAdder(val program: Program, val options: CompilationOptions, val
val possibleTargetDt = it.first.possibleDatatypes.first() val possibleTargetDt = it.first.possibleDatatypes.first()
val targetDt = if(possibleTargetDt.isPointer) BaseDataType.UWORD else possibleTargetDt // use UWORD instead of a pointer type (using words for pointers is allowed without further casting) val targetDt = if(possibleTargetDt.isPointer) BaseDataType.UWORD else possibleTargetDt // use UWORD instead of a pointer type (using words for pointers is allowed without further casting)
val argIdt = it.second.inferType(program) val argIdt = it.second.inferType(program)
if (argIdt.isKnown) { if (argIdt.isKnown && !targetDt.isStructInstance) {
val argDt = argIdt.getOrUndef() val argDt = argIdt.getOrUndef()
if (argDt.base !in it.first.possibleDatatypes) { if (argDt.base !in it.first.possibleDatatypes) {
val identifier = it.second as? IdentifierReference val identifier = it.second as? IdentifierReference

View File

@@ -447,6 +447,14 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
return listOf(IAstModification.ReplaceNode(functionCallExpr, cast, parent)) return listOf(IAstModification.ReplaceNode(functionCallExpr, cast, parent))
} }
} }
if(parent is IStatementContainer) {
val targetStruct = functionCallExpr.target.targetStructDecl()
if (targetStruct != null) {
// static struct instance allocation can only occur as an initializer for a pointer variable
return listOf(IAstModification.Remove(functionCallExpr, parent as IStatementContainer))
}
}
return noModifications return noModifications
} }

View File

@@ -1,11 +1,18 @@
package prog8tests.compiler package prog8tests.compiler
import io.kotest.assertions.withClue
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.engine.spec.tempdir import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.types.instanceOf
import prog8.code.ast.PtReturn
import prog8.code.ast.PtSubSignature
import prog8.code.target.C64Target import prog8.code.target.C64Target
import prog8.code.target.VMTarget import prog8.code.target.VMTarget
import prog8.vm.VmRunner import prog8.vm.VmRunner
import prog8tests.helpers.ErrorReporterForTests
import prog8tests.helpers.compileText import prog8tests.helpers.compileText
import kotlin.io.path.readText import kotlin.io.path.readText
@@ -38,4 +45,169 @@ thing {
VmRunner().runProgram(virtfile.readText(), true) VmRunner().runProgram(virtfile.readText(), true)
} }
test("passing struct instances to subroutines and returning a struct instance is not allowed") {
val src="""
main {
sub start() {
}
struct Node {
bool flag
}
sub faulty(Node arg) -> Node {
return cx16.r0
}
}
"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors=errors)
val err = errors.errors
err.size shouldBe 3
err[0] shouldContain "uword doesn't match"
err[1] shouldContain "structs can only be passed via a pointer"
err[2] shouldContain "structs can only be returned via a pointer"
}
test("pointers in subroutine return values") {
val src="""
main {
sub start() {
^^thing.Node @shared ptr = thing.new()
}
}
thing {
struct Node {
bool flag
^^Node next
}
sub new() -> ^^Node {
cx16.r0++
^^Node pointer = 2000
return pointer
}
}"""
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
// TODO compileText(C64Target(), false, src, outputDir) shouldNotBe null
}
test("creating instances") {
val src="""
main {
struct MyNode {
bool flag
^^MyNode next
}
sub start() {
^^MyNode @shared m1 = MyNode()
^^MyNode @shared m2 = MyNode(true, 0)
^^thing.Node @shared n1 = thing.Node()
^^thing.Node @shared n2 = thing.Node(true, 0)
}
}
thing {
struct Node {
bool flag
^^Node next
}
}"""
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
compileText(VMTarget(), true, src, outputDir) shouldNotBe null
// TODO compileText(C64Target(), false, src, outputDir) shouldNotBe null
// TODO compileText(C64Target(), true, src, outputDir) shouldNotBe null
}
test("creating instances with optimization should all be removed") {
val src="""
main {
struct MyNode {
bool flag
^^MyNode next
}
sub start() {
^^MyNode m1 = MyNode()
^^MyNode m2 = MyNode(true, 0)
^^thing.Node n1 = thing.Node()
^^thing.Node n2 = thing.Node(true, 0)
}
}
thing {
struct Node {
bool flag
^^Node next
}
}"""
var result = compileText(VMTarget(), true, src, outputDir)!!
withClue("all variables should have been optimized away") {
val start = result.codegenAst!!.entrypoint()!!
start.children.size shouldBe 2
start.children[0] shouldBe instanceOf<PtSubSignature>()
start.children[1] shouldBe instanceOf<PtReturn>()
}
// TODO compileText(C64Target(), true, src, outputDir) shouldNotBe null
}
test("creating instances should have correct number of args") {
val src="""
main {
struct Node {
bool flag
ubyte value
^^Node next
}
sub start() {
^^Node ptr = Node(true) ; error
}
}"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors=errors)
val err = errors.errors
err.size shouldBe 1
err[0] shouldContain("expected 3 or 0, got 1")
}
test("pointer uword compatibility") {
val src="""
main {
struct MyNode {
bool flag
^^MyNode next
}
sub start() {
cx16.r0 = MyNode()
^^MyNode @shared ptr1 = cx16.r0
ptr1 = 2000
ptr1 = 20
ptr1 = 20.2222
}
}"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, errors=errors)
val err = errors.errors
err.size shouldBe 1
err[0] shouldContain("15:16: can only assign uword or correct pointer type to a pointer")
}
}) })

View File

@@ -73,7 +73,7 @@ class TestSubroutines: FunSpec({
func.statements.isEmpty() shouldBe true func.statements.isEmpty() shouldBe true
} }
test("cannot call a subroutine via pointer") { test("cannot call a subroutine via a pointer") {
val src=""" val src="""
main { main {
sub start() { sub start() {

View File

@@ -1458,7 +1458,8 @@ class FunctionCallExpression(override var target: IdentifierReference,
return InferredTypes.unknown() // has multiple return types... so not a single resulting datatype possible return InferredTypes.unknown() // has multiple return types... so not a single resulting datatype possible
} }
is StructDecl -> { is StructDecl -> {
return InferredTypes.knownFor(DataType.structInstance(stmt)) // calling a struct is syntax for allocating a static instance, and returns a pointer to that (not the instance itself)
return InferredTypes.knownFor(DataType.pointerToType(stmt))
} }
else -> return InferredTypes.unknown() else -> return InferredTypes.unknown()
} }

View File

@@ -109,7 +109,10 @@ object InferredTypes {
} }
type.isPointerArray -> InferredType.known(DataType.arrayOfPointersTo(type.sub, type.subType)) type.isPointerArray -> InferredType.known(DataType.arrayOfPointersTo(type.sub, type.subType))
type.isStructInstance -> { type.isStructInstance -> {
InferredType.known(DataType.structInstance(type.subType!!)) if(type.subType!=null)
InferredType.known(DataType.structInstance(type.subType!!))
else
InferredType.known(DataType.structInstanceFromAntlr(type.subTypeFromAntlr!!))
} }
else -> throw IllegalArgumentException("invalid type $type") else -> throw IllegalArgumentException("invalid type $type")
} }

View File

@@ -1,47 +1,16 @@
%import textio
main { main {
struct MyNode {
bool flag
^^MyNode next
}
sub start() { sub start() {
^^uword ptr = 2000 cx16.r0 = MyNode()
ptr^^ <<= 2 ^^MyNode @shared ptr1 = cx16.r0
ptr^^ >>= 3
pokew(2000, 1111) ptr1 = 2000
ptr1 = 20
cx16.r0 ^= $ffff ptr1 = 20.2222
txt.print_uw(ptr^^)
txt.nl()
ptr^^ += 5
txt.print_uw(ptr^^)
txt.nl()
ptr^^ -= 9
txt.print_uw(ptr^^)
txt.nl()
ptr^^ *= 3
txt.print_uw(ptr^^)
txt.nl()
ptr^^ /= 3
txt.print_uw(ptr^^)
txt.nl()
ptr^^ |= $7f0f
txt.print_uwhex(ptr^^,true)
txt.nl()
ptr^^ &= $f0f0
txt.print_uwhex(ptr^^,true)
txt.nl()
ptr^^ ^= $ffff
txt.print_uwhex(ptr^^,true)
txt.nl()
; const uword buffer = $2000
; uword @shared addr = &buffer[2]
;
; const ubyte width = 100
; ubyte @shared i
; ubyte @shared j
; uword @shared addr2 = &buffer[i * width + j]
; txt.print_uw(addr)
} }
} }

View File

@@ -1,10 +1,10 @@
package prog8.intermediate package prog8.intermediate
import prog8.code.INTERNED_STRINGS_MODULENAME import prog8.code.INTERNED_STRINGS_MODULENAME
import prog8.code.core.BaseDataType
import prog8.code.core.DataType import prog8.code.core.DataType
import prog8.code.core.Encoding import prog8.code.core.Encoding
import prog8.code.core.ZeropageWish import prog8.code.core.ZeropageWish
import prog8.code.core.BaseDataType
// In the Intermediate Representation, all nesting has been removed. // In the Intermediate Representation, all nesting has been removed.
@@ -30,6 +30,9 @@ class IRSymbolTable {
fun allStructInstances(): Sequence<IRStStructInstance> = fun allStructInstances(): Sequence<IRStStructInstance> =
table.asSequence().map { it.value }.filterIsInstance<IRStStructInstance>() table.asSequence().map { it.value }.filterIsInstance<IRStStructInstance>()
fun allStructDefs(): Sequence<IRStStructDef> =
table.asSequence().map { it.value }.filterIsInstance<IRStStructDef>()
fun lookup(name: String): IRStNode? = table[name] fun lookup(name: String): IRStNode? = table[name]
fun add(node: IRStNode) { fun add(node: IRStNode) {