diff --git a/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt b/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt index 50296a8a3..db1a6938c 100644 --- a/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt +++ b/codeGenIntermediate/src/prog8/codegen/intermediate/ExpressionGen.kt @@ -901,9 +901,27 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) { } } BaseDataType.POINTER -> { - require(valueDt.isUnsignedWord || valueDt.isPointer || valueDt.isLong) - actualResultReg2 = tr.resultReg - // no further conversion required, pointers are all just uword or long + when(valueDt.base) { + BaseDataType.BOOL, BaseDataType.UBYTE -> { + // ubyte to pointer: zero extend + actualResultReg2 = codeGen.registers.next(IRDataType.WORD) + addInstr(result, IRInstruction(Opcode.EXT, type = IRDataType.BYTE, reg1 = actualResultReg2, reg2=tr.resultReg), null) + } + BaseDataType.BYTE -> { + // byte to pointer: sign extend + actualResultReg2 = codeGen.registers.next(IRDataType.WORD) + addInstr(result, IRInstruction(Opcode.EXTS, type = IRDataType.BYTE, reg1 = actualResultReg2, reg2=tr.resultReg), null) + } + BaseDataType.WORD -> { + // word to pointer: same size, just reinterpret + actualResultReg2 = tr.resultReg + } + else -> { + require(valueDt.isUnsignedWord || valueDt.isPointer || valueDt.isLong) + actualResultReg2 = tr.resultReg + // no further conversion required, pointers are all just uwords or longs + } + } } BaseDataType.ARRAY_POINTER -> { TODO("typecast to array of pointers $valueDt -> ${cast.type} ${cast.position}") diff --git a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt index 7e2363ae7..e80b9bd97 100644 --- a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt +++ b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt @@ -1305,7 +1305,7 @@ _after: val isAugmentedPattern = isAugmentedMemoryPattern(assignment.value, address, deref) if(isAugmentedPattern) { val memwrite = DirectMemoryWrite(address, deref.position) - val target = AssignTarget(null, null, memwrite, null, false, null, null, deref.position) + val target = AssignTarget(null, null, memwrite, null, false, position = deref.position) val newValue = convertAugmentedValueToMemoryRead(assignment.value, deref, address) val newAssignment = Assignment(target, newValue, assignment.origin, assignment.position) newAssignment.isAugmentedMemoryAssign = true @@ -1423,6 +1423,125 @@ _after: return noModifications } + override fun before(assignTarget: AssignTarget, parent: Node): Iterable { + if(assignTarget.dotExpression==null) + return noModifications + if(parent !is Assignment && parent !is ChainedAssignment) + errors.err("cannot use a dereferenced expression as assignment target here", assignTarget.position) + return noModifications + } + + override fun before(assignment: Assignment, parent: Node): Iterable { + val target = assignment.target + val dotExpr = target.dotExpression ?: return noModifications + + // decompose the '.' chain into the base expression and the field names + val fields = mutableListOf() + var base = dotExpr + while(base is BinaryExpression && base.operator==".") { + val rightIdent = base.right as? IdentifierReference ?: return noModifications + fields.add(rightIdent) + base = base.left + } + fields.reverse() + + // Evaluate the base expression exactly once into a temporary variable, so that the + // base is desugared in its own normal assignment context and the address computation + // 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 + } + val tmpVar = VarDecl.createAuto(DataType.UWORD, 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) + baseCast.expression.copy() + else + TypecastExpression(base.copy(), DataType.UWORD, false, base.position) + val tmpAssign = Assignment( + AssignTarget(tmpIdent.copy(), null, null, null, false, position=tmpVar.position), + baseValue, + AssignmentOrigin.USERCODE, base.position) + + // build the address of the final field; intermediate pointer fields in the chain are loaded via peekw + var address: Expression = tmpIdent.copy() + var currentStruct: StructDecl = struct + var fieldDt: DataType? = null + for((index, field) in fields.withIndex()) { + val fieldName = field.nameInSource.single() + fieldDt = currentStruct.getFieldType(fieldName) + if(fieldDt==null) { + errors.err("no such field '$fieldName' in struct '${currentStruct.name}'", field.position) + return noModifications + } + val offset = currentStruct.offsetof(fieldName, program.target)!!.toInt() + if(offset>0) + address = BinaryExpression(address, "+", NumericLiteral.optimalInteger(offset, field.position), field.position) + if(index < fields.size-1) { + if(!fieldDt.isPointer) { + errors.err("cannot dereference non-pointer field '$fieldName'", field.position) + return noModifications + } + val nextStruct = fieldDt.subType as? StructDecl + if(nextStruct==null) { + errors.err("cannot dereference field '$fieldName', expected a pointer to a struct", field.position) + return noModifications + } + val peekw = FunctionCallExpression(IdentifierReference(listOf("peekw"), field.position), mutableListOf(address), field.position) + address = TypecastExpression(peekw, DataType.UWORD, false, field.position) + currentStruct = nextStruct + } + } + + // select the appropriate poke/peek routine pair for the field's datatype + val dt = fieldDt!! + val funcName: String + val pokeCast: DataType? + val peekFuncName: String + val peekCast: DataType? + when { + dt.isBool -> { funcName="pokebool"; pokeCast=null; peekFuncName="peekbool"; peekCast=null } + dt.isUnsignedByte -> { funcName="poke"; pokeCast=null; peekFuncName="peek"; peekCast=null } + dt.isSignedByte -> { funcName="poke"; pokeCast=DataType.UBYTE; peekFuncName="peek"; peekCast=DataType.BYTE } + dt.isUnsignedWord -> { funcName="pokew"; pokeCast=null; peekFuncName="peekw"; peekCast=null } + dt.isSignedWord -> { funcName="pokew"; pokeCast=DataType.UWORD; peekFuncName="peekw"; peekCast=DataType.WORD } + dt.isLong -> { funcName="pokel"; pokeCast=null; peekFuncName="peekl"; peekCast=null } + dt.isFloat -> { funcName="pokef"; pokeCast=null; peekFuncName="peekf"; peekCast=null } + dt.isPointer -> { funcName="pokew"; pokeCast=null; peekFuncName="peekw"; peekCast=dt } + else -> { + errors.err("unsupported field datatype $dt for write through a pointer", target.position) + return noModifications + } + } + + // augmented assignment pattern: the value starts with (a copy of) the very same dotted chain, + // e.g. "(p).bar += 1". Replace that copy by a read of the field through the temporary address, + // so that everything is evaluated exactly once. + var valueExpr = assignment.value + val binValue = valueExpr as? BinaryExpression + if(binValue!=null && binValue.left isSameAs dotExpr) { + val peekCall = FunctionCallExpression( + IdentifierReference(listOf(peekFuncName), binValue.left.position), + mutableListOf(address.copy()), binValue.left.position) + val readExpr: Expression = if(peekCast==null) peekCall else TypecastExpression(peekCall, peekCast, false, peekCall.position) + valueExpr = BinaryExpression(readExpr, binValue.operator, binValue.right, binValue.position) + } + val value = if(pokeCast==null) valueExpr else TypecastExpression(valueExpr, pokeCast, false, valueExpr.position) + + 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)) + } + override fun after(assignment: Assignment, parent: Node): Iterable { val targetDt = assignment.target.inferType(program) val sourceDt = assignment.value.inferType(program) diff --git a/compiler/test/TestPointers.kt b/compiler/test/TestPointers.kt index fceea7bce..c8aac4d64 100644 --- a/compiler/test/TestPointers.kt +++ b/compiler/test/TestPointers.kt @@ -1592,7 +1592,7 @@ main { compileText(C64Target(), false, src, outputDir) shouldNotBe null } - xtest("array indexed assignment parses with and without explicit dereference after struct pointer [IGNORED because it's a parser error right now]") { + test("array indexed assignment without explicit dereference after struct pointer") { val src=""" main { @@ -1604,14 +1604,27 @@ main { ^^Node l1 l1.s[0] = 4242 + } +}""" + compileText(VMTarget(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + } + + xtest("array indexed assignment WITH explicit dereference after struct pointer [IGNORED because it's still a parser error: ^^ cannot be followed by an index in an assignment target]") { + val src=""" +main { + + sub start() { + struct Node { + ^^uword s + } + + ^^Node l1 + l1^^.s[0] = 4242 ; TODO fix parse error } }""" - val errors = ErrorReporterForTests(keepMessagesAfterReporting = true) - compileText(VMTarget(), false, src, outputDir, errors = errors) shouldBe null - errors.errors.size shouldBe 2 - errors.errors[0] shouldContain "no support for" - errors.errors[1] shouldContain "no support for" + compileText(VMTarget(), false, src, outputDir) shouldBe null } xtest("a.b.c[i].value = X where pointer is struct gives good error message [IGNORED because it's a parser error right now]") { @@ -3296,7 +3309,7 @@ main { compileText(Cx16Target(), false, src, outputDir) shouldNotBe null } - xtest("function call returning pointer - field access chaining as assignment target [nasty parser problem]") { + test("function call returning pointer - field access chaining as assignment target") { val src=""" main { struct Thing { @@ -3317,9 +3330,99 @@ main { getThing().next.port = 5678 } }""" - // currently fails at parse stage, should eventually compile for both VM and 6502 without needing assembly generation - compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null - compileText(Cx16Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null + // compiles for both VM and 6502 without needing assembly generation + compileText(VMTarget(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + compileText(C64Target(), false, src, outputDir) shouldNotBe null + } + + test("dereference-after-cast writes to every numeric field datatype compiles") { + val src=""" +%import floats + +main { + struct SBool { bool f } + struct SByte { byte f } + struct SUByte { ubyte f } + struct SWord { word f } + struct SUWord { uword f } + struct SLong { long f } + struct SFloat { float f } + + sub start() { + uword[8] ptrs + ptrs[0] = $4510 + ptrs[1] = $4518 + ptrs[2] = $4520 + ptrs[3] = $4528 + ptrs[4] = $4530 + ptrs[5] = $4540 + ptrs[6] = $4550 + + (ptrs[0] as ^^SBool).f = true + (ptrs[1] as ^^SByte).f = -42 + (ptrs[2] as ^^SUByte).f = 200 + (ptrs[3] as ^^SWord).f = -1000 + (ptrs[4] as ^^SUWord).f = 50000 + (ptrs[5] as ^^SLong).f = 100000 + (ptrs[6] as ^^SFloat).f = 3.14 + + ; augmented variants exercise the read-modify-write path per datatype + (ptrs[0] as ^^SBool).f = false + (ptrs[1] as ^^SByte).f += 10 + (ptrs[3] as ^^SWord).f -= 500 + (ptrs[4] as ^^SUWord).f |= $000f + (ptrs[2] as ^^SUByte).f++ + (ptrs[2] as ^^SUByte).f-- + } +}""" + compileText(VMTarget(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + compileText(C64Target(), false, src, outputDir) shouldNotBe null + } + + test("postincrement and postdecrement through dereferenced expression compiles") { + val src=""" +main { + struct Counter { + uword n + ubyte small + } + + sub start() { + uword[2] ptrs + ptrs[0] = $4560 + ptrs[1] = $4568 + + (ptrs[0] as ^^Counter).n++ + (ptrs[0] as ^^Counter).n-- + (ptrs[1] as ^^Counter).small++ + (ptrs[0] as ^^Counter).n += 3 + (ptrs[0] as ^^Counter).n -= 1 + (ptrs[1] as ^^Counter).small |= 2 + } +}""" + compileText(VMTarget(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + compileText(C64Target(), false, src, outputDir) shouldNotBe null + } + + test("parenthesized plain variable assignment target flattens to normal handling") { + val src=""" +main { + struct Foo { + ubyte bar + } + + sub start() { + ^^Foo fp = $4570 + (fp).bar = 55 + (fp).bar += 2 + } +}""" + compileText(VMTarget(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + compileText(C64Target(), false, src, outputDir) shouldNotBe null } test("function call returning pointer - field access chaining as value (RHS) works") { @@ -3352,4 +3455,169 @@ main { compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null compileText(Cx16Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null } + + test("dereference-after-cast assignment parses for all targets") { + val src=""" +main { + struct Foo { + ubyte bar + uword big + } + + sub start() { + uword[4] ptrs + ptrs[2] = $4300 + (ptrs[2] as ^^Foo).bar = 42 + (ptrs[2] as ^^Foo).big = 1234 + ($4300 as ^^Foo).bar = 1 + } +}""" + compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null + compileText(C64Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null + } + + test("functioncall deref chain as assignment target parses and compiles") { + val src=""" +main { + struct Thing { + uword port + ^^Thing next + bool flag + } + + sub getThing() -> ^^Thing { + return 0 + } + + sub start() { + getThing().flag = true + getThing().next.flag = false + getThing().port = 1234 + } +}""" + compileText(VMTarget(), false, src, outputDir, writeAssembly = false) shouldNotBe null + compileText(C64Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir, writeAssembly = false) shouldNotBe null + } + + test("dereference-after-cast assignments write correct memory on VM") { + val src=$$""" +%zeropage basicsafe +main { + struct Inner { + ubyte deep + } + struct Foo { + ubyte bar + ubyte baz + ^^Inner inner + uword big + } + + sub start() { + uword[4] ptrs + ptrs[2] = $4300 + ptrs[3] = $4310 + + ; plain write through dereference-after-cast + (ptrs[2] as ^^Foo).bar = 42 + + ; augmented write through dereference-after-cast + (ptrs[2] as ^^Foo).baz = 0 + (ptrs[2] as ^^Foo).baz += 5 + + ; chained through intermediate pointer field + (ptrs[2] as ^^Foo).inner = ptrs[3] + ((ptrs[2] as ^^Foo).inner).deep = 77 + + ; word-sized field + (ptrs[2] as ^^Foo).big = 1234 + + ; verify the writes landed in raw memory, independent of struct layout details + if peek($4300 + offsetof(Foo.bar)) != 42 { sys.exit(7) } + if peek($4300 + offsetof(Foo.baz)) != 5 { sys.exit(8) } + if peekw($4300 + offsetof(Foo.inner)) != $4310 { sys.exit(9) } + if peek($4310 + offsetof(Inner.deep)) != 77 { sys.exit(10) } + if peekw($4300 + offsetof(Foo.big)) != 1234 { sys.exit(11) } + + ; success marker (exit status is not observable through VmRunner) + pokew($4400, $c0ba) + sys.exit(0) + } +}""" + val result = compileText(VMTarget(), false, src, outputDir)!! + val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir") + VmRunner().runAndTestProgram(virtfile.readText(), true) { vm -> + vm.memory.getUB(0x4300u).toInt() shouldBe 42 // bar + vm.memory.getUB(0x4301u).toInt() shouldBe 5 // baz was 0, += 5 + vm.memory.getUW(0x4302u).toInt() shouldBe 0x4310 // inner pointer field + vm.memory.getUB(0x4310u).toInt() shouldBe 77 // chained deep write + vm.memory.getUW(0x4400u).toInt() shouldBe 0xc0ba // all in-program raw-memory checks passed + } + } + + test("functioncall deref chain assignment writes correct memory on VM") { + val src=""" +%zeropage basicsafe +main { + struct Thing { + uword port + bool flag + } + + pointer theptr + + sub getThing() -> ^^Thing { + return theptr as ^^Thing + } + + sub start() { + theptr = $4400 + getThing().port = 5678 + getThing().flag = true + sys.exit(0) + } +}""" + val result = compileText(VMTarget(), false, src, outputDir)!! + val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir") + VmRunner().runAndTestProgram(virtfile.readText(), true) { vm -> + vm.memory.getUW(0x4400u).toInt() shouldBe 5678 + vm.memory.getUB(0x4402u).toInt() shouldBe 1 + } + } + + test("assignment through dereferenced non-struct pointer gives error") { + val src=""" +main { + sub start() { + uword[4] ptrs + ptrs[2] = $4300 + (ptrs[2]).bar = 42 + } +}""" + val errors = ErrorReporterForTests(keepMessagesAfterReporting = true) + compileText(VMTarget(), false, src, outputDir, errors = errors) shouldBe null + errors.errors.size shouldBe 1 + errors.errors[0] shouldContain "expected a pointer to a struct" + } + + test("assignment through dereferenced expression with unknown field gives error") { + val src=""" +main { + struct Foo { + ubyte bar + } + + sub start() { + uword[4] ptrs + ptrs[2] = $4300 + (ptrs[2] as ^^Foo).nosuchfield = 42 + } +}""" + val errors = ErrorReporterForTests(keepMessagesAfterReporting = true) + compileText(VMTarget(), false, src, outputDir, errors = errors) shouldBe null + errors.errors.size shouldBe 1 + errors.errors[0] shouldContain "no such field" + } }) diff --git a/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt b/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt index 466f16c85..30e6e1f8c 100644 --- a/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt +++ b/compilerAst/src/prog8/ast/AstToSourceTextConverter.kt @@ -510,6 +510,7 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program: assignTarget.arrayindexed?.accept(this) assignTarget.pointerDereference?.accept(this) assignTarget.arrayIndexedDereference?.accept(this) + assignTarget.dotExpression?.accept(this) val multi = assignTarget.multi if (multi != null) { multi.dropLast(1).forEach { target -> diff --git a/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt b/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt index e2d32dde4..fca39cd28 100644 --- a/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt +++ b/compilerAst/src/prog8/ast/antlr/Antlr2KotlinVisitor.kt @@ -802,6 +802,33 @@ class Antlr2KotlinVisitor(val source: SourceCode, private val target: ICompilati } } + // (expr).field or (expr).field.sub as assignment target. + // A parenthesized plain (scoped) identifier is flattened into just that scoped name, + // so that "(a).b.c" is represented exactly like "a.b.c". + // All other expressions go into the dotExpression field; the CodeDesugarer turns + // those into poke-style writes. + override fun visitParenDerefTarget(ctx: ParenDerefTargetContext): AssignTarget { + val inner = ctx.expression().accept(this) + val fieldNames = ctx.identifier().map { it.text } + if(inner is IdentifierReference) { + return AssignTarget(IdentifierReference(inner.nameInSource + fieldNames, ctx.toPosition()), null, null, null, false, position = ctx.toPosition()) + } + var expr = inner as Expression + for(field in fieldNames) { + expr = BinaryExpression(expr, ".", IdentifierReference(listOf(field), ctx.toPosition()), ctx.toPosition()) + } + return AssignTarget(null, null, null, null, false, dotExpression = expr, position = ctx.toPosition()) + } + + // func().field as assignment target; the CodeDesugarer turns this into a poke-style write. + override fun visitFunctioncallDerefTarget(ctx: FunctioncallDerefTargetContext): AssignTarget { + var expr: Expression = ctx.functioncall().accept(this) as Expression + for(field in ctx.identifier()) { + expr = BinaryExpression(expr, ".", IdentifierReference(listOf(field.text), ctx.toPosition()), ctx.toPosition()) + } + return AssignTarget(null, null, null, null, false, dotExpression = expr, position = ctx.toPosition()) + } + /** * Visits a pointer dereference expression. * diff --git a/compilerAst/src/prog8/ast/statements/AstStatements.kt b/compilerAst/src/prog8/ast/statements/AstStatements.kt index 7cfacc7ca..f19478211 100644 --- a/compilerAst/src/prog8/ast/statements/AstStatements.kt +++ b/compilerAst/src/prog8/ast/statements/AstStatements.kt @@ -812,6 +812,7 @@ data class AssignTarget( val void: Boolean, var pointerDereference: PtrDereference? = null, var arrayIndexedDereference: ArrayIndexedPtrDereference? = null, + var dotExpression: Expression? = null, override val position: Position ) : Node { override lateinit var parent: Node @@ -823,6 +824,7 @@ data class AssignTarget( memoryAddress?.linkParents(this) pointerDereference?.linkParents(this) arrayIndexedDereference?.linkParents(this) + dotExpression?.linkParents(this) multi?.forEach { it.linkParents(this) } } @@ -884,6 +886,16 @@ data class AssignTarget( else -> throw FatalAstException("invalid replacement for AssignTarget.arrayIndexedDereference: $replacement") } } + node === dotExpression -> { + when (replacement) { + is Expression -> dotExpression = replacement + is IdentifierReference -> { + dotExpression = null + identifier = replacement + } + else -> throw FatalAstException("invalid replacement for AssignTarget.dotExpression: $replacement") + } + } node === multi -> throw FatalAstException("can't replace multi assign targets") else -> throw FatalAstException("invalid replace $node $replacement") } @@ -913,6 +925,7 @@ data class AssignTarget( void, pointerDereference?.copy(), arrayIndexedDereference?.copy(), + dotExpression?.copy(), position ) override fun referencesIdentifier(nameInSource: List): Boolean = @@ -921,6 +934,7 @@ data class AssignTarget( memoryAddress?.referencesIdentifier(nameInSource)==true || pointerDereference?.referencesIdentifier(nameInSource)==true || arrayIndexedDereference?.referencesIdentifier(nameInSource)==true || + dotExpression?.referencesIdentifier(nameInSource)==true || multi?.any { it.referencesIdentifier(nameInSource)}==true fun inferType(program: Program): InferredTypes.InferredType { @@ -934,6 +948,7 @@ data class AssignTarget( memoryAddress != null -> InferredTypes.knownFor(BaseDataType.UBYTE) pointerDereference != null -> pointerDereference!!.inferType(program) arrayIndexedDereference != null -> arrayIndexedDereference!!.inferType(program) + dotExpression != null -> dotExpression!!.inferType(program) else -> InferredTypes.unknown() // a multi-target has no 1 particular type } } @@ -947,6 +962,7 @@ data class AssignTarget( multi != null -> throw FatalAstException("cannot turn a multi-assign into a single source expression") pointerDereference != null -> pointerDereference!!.copy() arrayIndexedDereference != null -> arrayIndexedDereference!!.copy() + dotExpression != null -> dotExpression!!.copy() else -> throw FatalAstException("invalid assignment target") } } diff --git a/compilerAst/src/prog8/ast/walk/AstWalker.kt b/compilerAst/src/prog8/ast/walk/AstWalker.kt index f86482b77..39c83822d 100644 --- a/compilerAst/src/prog8/ast/walk/AstWalker.kt +++ b/compilerAst/src/prog8/ast/walk/AstWalker.kt @@ -569,6 +569,7 @@ abstract class AstWalker { assignTarget.memoryAddress?.accept(this, assignTarget) assignTarget.pointerDereference?.accept(this, assignTarget) assignTarget.arrayIndexedDereference?.accept(this, assignTarget) + assignTarget.dotExpression?.accept(this, assignTarget) assignTarget.multi?.forEach { it.accept(this, assignTarget) } track(after(assignTarget, parent), assignTarget, parent) } diff --git a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt index 9689a62c5..38f16d5a2 100644 --- a/compilerAst/src/prog8/ast/walk/IAstVisitor.kt +++ b/compilerAst/src/prog8/ast/walk/IAstVisitor.kt @@ -188,6 +188,7 @@ interface IAstVisitor { assignTarget.memoryAddress?.accept(this) assignTarget.pointerDereference?.accept(this) assignTarget.arrayIndexedDereference?.accept(this) + assignTarget.dotExpression?.accept(this) assignTarget.multi?.forEach { it.accept(this) } } diff --git a/docs/source/structpointers.rst b/docs/source/structpointers.rst index d061e9b01..48bafe43f 100644 --- a/docs/source/structpointers.rst +++ b/docs/source/structpointers.rst @@ -38,6 +38,12 @@ Structs and Pointers ^^Node thenode = &&np[2] thenode.field = 9999 + Writing through a parenthesized casting expression or a function call does work however; + the compiler evaluates the expression once and stores through the computed address:: + + (np[2] as ^^Node).field = 9999 + get_node(i).field = 9999 + Untyped pointers and the ``pointer`` type ----------------------------------------- @@ -168,6 +174,15 @@ Because it implies pointer dereferencing you can usually omit the explicit `^^`, if e1^^.ypos > 300 e1^^.health -= 10 +You can also assign to a struct field through an arbitrary pointer expression by wrapping it in parentheses. +This works with typecasts and with function calls that return a pointer:: + + (enemies[i] as ^^Enemy).health = 42 + (get_enemy(index)).shield -= 10 + get_enemy(index).next.target = 0 + +The compiler evaluates the pointer expression exactly once and then stores the value through the computed address. + .. note:: Structs are currently only supported as a *reference type* (they always have to be accessed through a pointer). diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 8a7739fcf..566ab0376 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -14,7 +14,7 @@ Future Things and Ideas - symboldump: some sort of javadocs generated from the p8 source files (instead of just the function signatures). Use markdown for formatting, not html. - if implementing unsigned longs: remove the (multiple?) "TODO "hack" to allow unsigned long constants to be used as values for signed longs, without needing a cast - struct/ptr: really fixing the pointer dereferencing issues (cursed hybrid between IdentifierReference, PtrDereferece and PtrIndexedDereference) may require getting rid of scoped identifiers altogether and treat '.' as a "scope or pointer following operator" -- struct/ptr: (later, nasty parser problem:) support chaining pointer dereference on function calls that return a pointer. (type checking now fails on stuff like func().field and func().next.field) +- struct/ptr: support chaining pointer dereference without explicit ^^ on assignment targets, such as ``l1.s[0] = 4242`` and ``listarray[2].value = 123`` (implicit ``^^`` forms; see TestPointers xtests and ideas/NEW-POINTERDEREF-PLANS.md). Note: the LHS functioncall and parenthesized-expression cases (``func().field = a``, ``(expr as ^^T).field = a``) are now supported: the assign_target grammar rule accepts them and the CodeDesugarer rewrites them into poke-style writes. - add documentation for more library modules instead of just linking to the source code - add float support to the configurable compiler targets. Restrictions: just have "cbm-style floats" as an option (to that it can slot into the current float codegen), where "all" you have to specify is the addresses of AYINT and GIVAYF and FADDT and all their friends. - Change scoping rules for qualified symbols so that they don't always start from the root but behave like other programming languages (look in local scope first), maybe only when qualified symbol starts with '.' such as: .local.value = 33, or the other way around? i.e. require new syntax to explicitly look up from global scope. That would give a backwards compatible solution. diff --git a/ideas/NEW-POINTERDEREF-PLANS.md b/ideas/NEW-POINTERDEREF-PLANS.md index a6baecde9..005143b3e 100644 --- a/ideas/NEW-POINTERDEREF-PLANS.md +++ b/ideas/NEW-POINTERDEREF-PLANS.md @@ -4,6 +4,14 @@ This document outlines the implementation plan to fix pointer dereference syntax limitations in Prog8. +> **Status update (2026-08):** A targeted subset has been implemented on master: +> assignment targets now accept parenthesized expressions and function calls followed by +> a field chain, e.g. `(expr as ^^Struct).field = value` and `func().field = value` +> (grammar rule `assign_target` alternatives `ParenDerefTarget` / `FunctioncallDerefTarget`, +> desugared by CodeDesugarer into poke-style writes). The implicit-`^^` forms described +> below (`ptr[idx].field = value`, `ptr^^.field[idx] = value`) are still unsupported and +> require the larger redesign (Option B) described in this document. + ### The Problem Currently, `^^` (pointer dereference) is required in grammar even when it's obvious from context: diff --git a/parser/src/main/antlr/Prog8ANTLR.g4 b/parser/src/main/antlr/Prog8ANTLR.g4 index d77d42b69..9dfd8ec47 100644 --- a/parser/src/main/antlr/Prog8ANTLR.g4 +++ b/parser/src/main/antlr/Prog8ANTLR.g4 @@ -243,6 +243,8 @@ assign_target: | arrayindexed #ArrayindexedTarget | directmemory #MemoryTarget | pointerdereference #PointerDereferenceTarget + | '(' expression ')' ('.' identifier)+ #ParenDerefTarget + | functioncall ('.' identifier)+ #FunctioncallDerefTarget | VOID #VoidTarget ;