diff --git a/README.md b/README.md index 79e79f35e..d322c87db 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ which aims to provide many conveniences over raw assembly code (even when using **Want to buy me a coffee or a pizza perhaps?** -This project was created over the last couple of years by dedicating a huge amoung of my free time to it, to make it the best I possibly can. +This project was created over the last couple of years by dedicating a huge amount of my free time to it, to make it the best I possibly can. If you like Prog8, and think it's worth a nice cup of hot coffee or a delicious pizza, you can help me out a little bit over at [ko-fi.com/irmen](https://ko-fi.com/irmen) or [PayPal](https://paypal.me/irmendejong) @@ -95,7 +95,7 @@ What does Prog8 provide? *Rapid edit-compile-run-debug cycle:* - use a modern PC to do the work on, use nice editors and enjoy quick compilation times -- can automatically run the program in the Vice emulator after succesful compilation +- can automatically run the program in the Vice emulator after successful compilation - breakpoints, that let the Vice emulator drop into the monitor if execution hits them - source code labels automatically loaded in Vice emulator so it can show them in disassembly diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt index c3ade63c6..b9f61e9d9 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/AssignmentAsmGen.kt @@ -5945,6 +5945,7 @@ $endLabel""") eor #255 sta ${target.asmVarname}+3""") } + TargetStorageKind.POINTER -> pointergen.inplaceLongInvert(PtrTarget(target)) else -> TODO("LONG INVERT ${target.kind} ${target.position}") } } diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt index 466982516..5641f3ccb 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt @@ -114,79 +114,117 @@ internal class PointerAssignmentsGen(private val asmgen: AsmGen6502Internal, pri internal fun inplaceByteInvert(target: PtrTarget) { val (zpPtrVar, offset) = deref(target.pointer) - asmgen.out(""" - ldy #$offset - lda ($zpPtrVar),y - eor #255 - sta ($zpPtrVar),y""") + val eorVal = if(target.dt.isBool) 1 else 255 + if(offset==0.toUByte() && asmgen.isTargetCpu(CpuType.CPU65C02)) { + asmgen.out(" lda ($zpPtrVar) | eor #$eorVal | sta ($zpPtrVar)") + } else { + asmgen.out(""" + ldy #$offset + lda ($zpPtrVar),y + eor #$eorVal + sta ($zpPtrVar),y""") + } } internal fun inplaceWordInvert(target: PtrTarget) { val (zpPtrVar, offset) = deref(target.pointer) asmgen.out(""" - ldy #$offset - lda ($zpPtrVar),y - eor #255 - sta ($zpPtrVar),y - iny - lda ($zpPtrVar),y - eor #255 - sta ($zpPtrVar),y""") + ldy #$offset + lda ($zpPtrVar),y + eor #255 + sta ($zpPtrVar),y + iny + lda ($zpPtrVar),y + eor #255 + sta ($zpPtrVar),y""") + } + + internal fun inplaceLongInvert(target: PtrTarget) { + val (zpPtrVar, offset) = deref(target.pointer) + asmgen.out(""" + ldy #$offset + lda ($zpPtrVar),y + eor #255 + sta ($zpPtrVar),y + iny + lda ($zpPtrVar),y + eor #255 + sta ($zpPtrVar),y + iny + lda ($zpPtrVar),y + eor #255 + sta ($zpPtrVar),y + iny + lda ($zpPtrVar),y + eor #255 + sta ($zpPtrVar),y""") } internal fun inplaceByteNegate(target: PtrTarget, scope: IPtSubroutine?) { val (zpPtrVar, offset) = deref(target.pointer) - asmgen.out(""" - ldy #$offset - lda #0 - sec - sbc ($zpPtrVar),y - sta ($zpPtrVar),y""") + if(asmgen.isTargetCpu(CpuType.CPU65C02)) { + asmgen.out(""" + ldy #$offset + lda ($zpPtrVar),y + eor #255 + ina + sta ($zpPtrVar),y""") + } else { + asmgen.out(""" + ldy #$offset + lda ($zpPtrVar),y + eor #255 + clc + adc #1 + sta ($zpPtrVar),y""") + } } internal fun inplaceWordNegate(target: PtrTarget, scope: IPtSubroutine?) { val (zpPtrVar, offset) = deref(target.pointer) asmgen.out(""" - ldy #$offset - lda #0 - sec - sbc ($zpPtrVar),y - sta ($zpPtrVar),y - iny - lda #0 - sbc ($zpPtrVar),y - sta ($zpPtrVar),y""") + ldy #$offset + sec + lda #0 + sbc ($zpPtrVar),y + sta ($zpPtrVar),y + iny + lda #0 + sbc ($zpPtrVar),y + sta ($zpPtrVar),y""") } internal fun inplaceLongNegate(target: PtrTarget, scope: IPtSubroutine?) { val (zpPtrVar, offset) = deref(target.pointer) asmgen.out(""" - ldy #$offset - lda #0 - sec - sbc ($zpPtrVar),y - sta ($zpPtrVar),y - iny - lda #0 - sbc ($zpPtrVar),y - sta ($zpPtrVar),y - iny - lda #0 - sbc ($zpPtrVar),y - sta ($zpPtrVar),y - iny - lda #0 - sbc ($zpPtrVar),y - sta ($zpPtrVar),y""") + ldy #$offset + sec + lda #0 + sbc ($zpPtrVar),y + sta ($zpPtrVar),y + iny + lda #0 + sbc ($zpPtrVar),y + sta ($zpPtrVar),y + iny + lda #0 + sbc ($zpPtrVar),y + sta ($zpPtrVar),y + iny + lda #0 + sbc ($zpPtrVar),y + sta ($zpPtrVar),y""") } internal fun inplaceFloatNegate(target: PtrTarget, scope: IPtSubroutine?) { - val (zpPtrVar, _) = deref(target.pointer, addOffsetToPointer=true) + val (zpPtrVar, offset) = deref(target.pointer) + // flip sign bit in second byte of 5-byte float asmgen.out(""" - ldy #1 - lda ($zpPtrVar),y - eor #$80 - sta ($zpPtrVar),y""") + ldy #$offset + iny + lda ($zpPtrVar),y + eor #${'$'}80 + sta ($zpPtrVar),y""") } diff --git a/codeOptimizers/src/prog8/optimizer/ConstantIdentifierReplacer.kt b/codeOptimizers/src/prog8/optimizer/ConstantIdentifierReplacer.kt index 7f1166116..c6ab85175 100644 --- a/codeOptimizers/src/prog8/optimizer/ConstantIdentifierReplacer.kt +++ b/codeOptimizers/src/prog8/optimizer/ConstantIdentifierReplacer.kt @@ -59,7 +59,7 @@ class VarConstantValueTypeAdjuster( if (decl.value != null && !isConstantInitializer(decl.value!!)) return noModifications if (decl.value != null && isMemorySlabInit(decl.value!!)) - return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferrably in zeropage) + return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferably in zeropage) val usages = callGraph.usages(decl) val (writes, reads) = usages .partition { diff --git a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt index 47b33432c..c9417912d 100644 --- a/compiler/src/prog8/compiler/astprocessing/AstChecker.kt +++ b/compiler/src/prog8/compiler/astprocessing/AstChecker.kt @@ -2212,7 +2212,7 @@ internal class AstChecker(private val program: Program, private fun checkPointer(pointervar: IdentifierReference) { val vardecl = pointervar.targetVarDecl() if(vardecl?.zeropage == ZeropageWish.NOT_IN_ZEROPAGE) - errors.info("pointer variable should preferrably be in zeropage but is marked nozp", vardecl.position) + errors.info("pointer variable should preferably be in zeropage but is marked nozp", vardecl.position) } override fun visit(arrayIndexedExpression: ArrayIndexedExpression) { diff --git a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt index c8d4307f4..3cf23777b 100644 --- a/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt +++ b/compiler/src/prog8/compiler/astprocessing/CodeDesugarer.kt @@ -628,7 +628,13 @@ _after: if(expr.operator==".") { if(expr.left is IdentifierReference) { - require(expr.right !is IdentifierReference) + if(expr.right is IdentifierReference) { + // (a.b).c -> a.b.c - merge dotted names (handles parenthesized pointer chains like (t.next).flag) + val leftIdent = expr.left as IdentifierReference + val rightIdent = expr.right as IdentifierReference + val combined = IdentifierReference(leftIdent.nameInSource + rightIdent.nameInSource, expr.position) + return listOf(AstReplaceNode(expr, combined, parent)) + } val ri = expr.right as? ArrayIndexedExpression if(ri!=null && ri.plainarrayvar!=null) { val leftIdent = expr.left as IdentifierReference @@ -687,6 +693,52 @@ _after: val ident = IdentifierReference(right.chain, right.position) return listOf(AstReplaceNode(expr.right, ident, expr)) } + + // Generic pointer field access on complex expressions, e.g. (expr as ^^Struct).field + // Simple identifier/array left-hand-sides (a.b.c, ptr[i].field) are handled elsewhere. + // Writes through such an expression can't occur: the grammar only allows them as read expressions. + if(expr.left !is IdentifierReference && expr.left !is ArrayIndexedExpression) { + val fieldIdent = expr.right as? IdentifierReference + if(fieldIdent!=null) { + val leftDt = expr.left.inferType(program).getOrUndef() + val struct = if(leftDt.isPointer) leftDt.subType as? StructDecl else null + val fieldName = fieldIdent.nameInSource.first() + val fieldDt = struct?.getFieldType(fieldName) + if(struct!=null && fieldDt!=null) { + // (ptr).field --> peekXXX(ptr as uword + offsetof(Struct.field)) + val offset = struct.offsetof(fieldName, program.target)!!.toInt() + val ptrAsUword = TypecastExpression(expr.left.copy(), DataType.UWORD, true, expr.left.position) + val address: Expression = if(offset==0) ptrAsUword + else BinaryExpression(ptrAsUword, "+", NumericLiteral.optimalInteger(offset, expr.position), expr.position) + fun peekCall(func: String) = + FunctionCallExpression(IdentifierReference(listOf(func), expr.position), mutableListOf(address), expr.position) + val readExpr: Expression = when { + fieldDt.isBool -> peekCall("peekbool") + fieldDt.isUnsignedByte -> DirectMemoryRead(address, expr.position) + fieldDt.isSignedByte -> TypecastExpression(DirectMemoryRead(address, expr.position), DataType.BYTE, true, expr.position) + fieldDt.isUnsignedWord -> peekCall("peekw") + fieldDt.isSignedWord -> TypecastExpression(peekCall("peekw"), DataType.WORD, true, expr.position) + fieldDt.isLong -> peekCall("peekl") + fieldDt.isFloat -> peekCall("peekf") + fieldDt.isPointer -> TypecastExpression(peekCall("peekw"), fieldDt, true, expr.position) + else -> { + errors.err("unsupported field type for pointer dereference", expr.position) + return noModifications + } + } + if(fieldIdent.nameInSource.size==1) + return listOf(AstReplaceNode(expr, readExpr, parent)) + // (ptr).field.rest... --> (read of first field) . rest... (will be desugared further in the next pass) + if(!fieldDt.isPointer) { + errors.err("cannot dereference non-pointer field '$fieldName'", fieldIdent.position) + return noModifications + } + val rest = IdentifierReference(fieldIdent.nameInSource.drop(1), fieldIdent.position) + val chained = BinaryExpression(readExpr, ".", rest, expr.position) + return listOf(AstReplaceNode(expr, chained, parent)) + } + } + } } return noModifications diff --git a/compiler/test/TestPointers.kt b/compiler/test/TestPointers.kt index b7a23c0ec..04f64b66b 100644 --- a/compiler/test/TestPointers.kt +++ b/compiler/test/TestPointers.kt @@ -3198,4 +3198,158 @@ main { compileText(Amiga500Target(), true, srcReturn, outputDir, writeAssembly = false) shouldNotBe null } + + test("parenthesized pointer field access desugars same as unparenthesized") { + val src=""" +main { + struct Thing { + uword port + ^^Thing next + bool flag + } + + sub start() { + ^^Thing @shared t + bool @shared d1 = t.next.flag + bool @shared d2 = (t.next).flag + ubyte @shared d3 = t.next.flag as ubyte + ubyte @shared d4 = (t.next).flag as ubyte + bool @shared d5 = t.next.next.flag + bool @shared d6 = (t.next).next.flag + } +}""" + val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!! + val assigns = result.compilerAst.entrypoint.statements.filterIsInstance() + .associateBy { it.target.identifier!!.nameInSource.last() } + + val d1 = assigns.getValue("d1").value as PtrDereference + d1.chain shouldBe listOf("t", "next", "flag") + (assigns.getValue("d2").value as PtrDereference).chain shouldBe d1.chain + ((assigns.getValue("d3").value as TypecastExpression).expression as PtrDereference).chain shouldBe d1.chain + ((assigns.getValue("d4").value as TypecastExpression).expression as PtrDereference).chain shouldBe d1.chain + val d5 = assigns.getValue("d5").value as PtrDereference + d5.chain shouldBe listOf("t", "next", "next", "flag") + (assigns.getValue("d6").value as PtrDereference).chain shouldBe d5.chain + + compileText(VMTarget(), true, src, outputDir) shouldNotBe null + compileText(C64Target(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + } + + test("field access on casted struct pointer expression") { + val src=""" +main { + struct Thing { + uword port + ^^Thing next + bool flag + } + + sub start() { + ^^Thing @shared t + bool @shared d1 = (t.port as ^^Thing).flag + ubyte @shared d2 = (t.port as ^^Thing).flag as ubyte + ^^Thing @shared d3 = (t.port as ^^Thing).next + bool @shared d4 = (t.port as ^^Thing).next.flag + bool @shared d5 = ((t.port as ^^Thing).next).flag + } +}""" + val errors = ErrorReporterForTests() + val result = compileText(VMTarget(), false, src, outputDir, errors, writeAssembly = false) + withClue(errors.errors.joinToString("\n")) { + result shouldNotBe null + } + val assigns = result!!.compilerAst.entrypoint.statements.filterIsInstance() + .associateBy { it.target.identifier!!.nameInSource.last() } + + // (t.port as ^^Thing).field --> peekXXX((t.port as ^^Thing as uword) + offsetof(field)) + val d1 = assigns.getValue("d1").value as FunctionCallExpression + d1.target.nameInSource shouldBe listOf("peekbool") + val d1addr = d1.args.single() as BinaryExpression + d1addr.operator shouldBe "+" + (d1addr.right as NumericLiteral).number shouldBe 4.0 // offsetof(Thing.flag) + + val d2 = assigns.getValue("d2").value as TypecastExpression + d2.type shouldBe DataType.UBYTE + (d2.expression as FunctionCallExpression).target.nameInSource shouldBe listOf("peekbool") + + // pointer-typed field read: peekw + cast back to the pointer type + val d3 = assigns.getValue("d3").value as TypecastExpression + d3.type.isPointer shouldBe true + val d3peek = d3.expression as FunctionCallExpression + d3peek.target.nameInSource shouldBe listOf("peekw") + ((d3peek.args.single() as BinaryExpression).right as NumericLiteral).number shouldBe 2.0 // offsetof(Thing.next) + + // chained: inner pointer-typed field is read via peekw first, then .flag via peekbool + val d4 = assigns.getValue("d4").value as FunctionCallExpression + d4.target.nameInSource shouldBe listOf("peekbool") + val d4addr = d4.args.single() as BinaryExpression + (d4addr.right as NumericLiteral).number shouldBe 4.0 + val d4inner = (d4addr.left as TypecastExpression).expression as TypecastExpression + (d4inner.expression as FunctionCallExpression).target.nameInSource shouldBe listOf("peekw") + + // explicit parenthesized chain desugars the same + (assigns.getValue("d5").value as FunctionCallExpression).target.nameInSource shouldBe listOf("peekbool") + + compileText(VMTarget(), true, src, outputDir) shouldNotBe null + compileText(C64Target(), false, src, outputDir) shouldNotBe null + compileText(Cx16Target(), false, src, outputDir) shouldNotBe null + } + + xtest("function call returning pointer - field access chaining as assignment target [nasty parser problem]") { + val src=""" +main { + struct Thing { + uword port + ^^Thing next + bool flag + } + + sub getThing() -> ^^Thing { + return 0 + } + + sub start() { + getThing().flag = true + getThing().next.flag = true + getThing().next.next.flag = false + getThing().port = 1234 + 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 + } + + test("function call returning pointer - field access chaining as value (RHS) works") { + val src=""" +main { + struct Thing { + uword port + ^^Thing next + bool flag + } + + sub getThing() -> ^^Thing { + return 0 + } + + sub start() { + bool a = getThing().flag + bool b = getThing().next.flag + bool c = getThing().next.next.flag + uword d = getThing().port + uword e = getThing().next.port + bool f = getThing().flag and getThing().next.flag + uword g = getThing().port + getThing().next.port + if getThing().flag { + a = true + } + } +}""" + // RHS value case already works 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 + } }) diff --git a/compilerAst/src/prog8/ast/expressions/AstExpressions.kt b/compilerAst/src/prog8/ast/expressions/AstExpressions.kt index 8a37f2418..2f0807c2a 100644 --- a/compilerAst/src/prog8/ast/expressions/AstExpressions.kt +++ b/compilerAst/src/prog8/ast/expressions/AstExpressions.kt @@ -276,7 +276,11 @@ class BinaryExpression( // SOMEEXPRESSION . NAME val leftDt = leftExpr.inferType(program) if (leftDt.isPointer) leftDt.getOrUndef().subType else null - } else null + } else { + // generic pointer expression . field (e.g. (expr as ^^Struct).field, or (ptrNext).field) + val leftDt = left.inferType(program) + if (leftDt.isPointer) leftDt.getOrUndef().subType else null + } if (struct == null) InferredTypes.unknown() else { diff --git a/docs/source/programming.rst b/docs/source/programming.rst index dc521edab..215cadaf5 100644 --- a/docs/source/programming.rst +++ b/docs/source/programming.rst @@ -125,7 +125,7 @@ scoped names always need to be fully scoped (because they always start in the gl .. index:: pair: Identifiers; Aliases The ``alias`` statement makes it easier to refer to symbols from other places, and they can save -you from having to type the fully scoped name everytime you need to access that symbol. +you from having to type the fully scoped name every time you need to access that symbol. Aliases can be created in any scope except at the module level. An alias is created with ``alias = `` and then you can use ```` as if it were ````. It is possible to alias variables, labels and subroutines, and even whole blocks and words @@ -196,7 +196,7 @@ to access a variable from a nested subroutine:: } **:index:`Aliases`** make it easier to refer to symbols from other places. They save -you from having to type the fully scoped name everytime you need to access that symbol. +you from having to type the fully scoped name every time you need to access that symbol. Aliases can be created in any scope except at the module level. You can create and use an alias with the ``alias`` statement like so:: diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 5be6004dc..7ea4b7623 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -1,8 +1,7 @@ TODO ==== -- "expression does not return a value" error: exec.FreeSignal((IOAudio3.ReplyPort as ^^exec.MsgPort).SigBit as byte) -- finish the Amiga high level audio interface, see examples/test.p8 for now +- finish the Amiga high level audio interface, see examples/amiga/bdmusic.p8 - need a bunch of type casting/conversion checks that test the handling of the 4-byte/long pointer datatype on the qemu68k target. - amiga library structs: use more typed pointers if it knows the struct type from the same (or another amiga library module) , rather than using `pointer`. Consider both the extsubs but also the struct fields in the amigaDOS structs in the generated library modules. diff --git a/examples/test.p8 b/examples/test.p8 index 909f5d77f..b065f34a9 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -1,65 +1,22 @@ -%import lists -%import textio -%zeropage basicsafe - main { + + struct Thing { + uword port + ^^Thing next + bool flag + } + sub start() { - reverseiter() - listiter() - } + ^^Thing @shared t - sub reverseiter() { - for x in "hello" { - txt.chrout(x) - } + ; this compiles fine: + bool @shared derp1 = t.next.flag + ubyte @shared derp2 = t.next.flag as ubyte - txt.nl() + bool @shared derp3 = (t.next).flag + ubyte @shared derp4 = (t.next).flag as ubyte - for x in "hello2" step -1 { - txt.chrout(x) - } - txt.nl() - - for y in [11,22,33,44] { - txt.print_ub(y) - txt.spc() - } - txt.nl() - - for y in [11,22,33,44] step -1 { - txt.print_ub(y) - txt.spc() - } - txt.nl() - } - - struct MyNode { - ^^MyNode Succ - ^^MyNode Pred - ubyte value - } - - struct MyList { - ^^MyNode Head - ^^MyNode Tail - ^^MyNode TailPred - } - - sub listiter() { - ^^MyList mylist = [] - ^^MyNode n1 = [0,0, 11] - ^^MyNode n2 = [0,0, 22] - ^^MyNode n3 = [0,0, 33] - list.init(mylist) - list.add_tail(mylist, n1) - list.add_tail(mylist, n2) - list.add_tail(mylist, n3) - - for node in mylist { - txt.print_ulhex(node as long, true) - txt.spc() - txt.print_ub(node.value) - txt.nl() - } + bool @shared derp5 = (t.port as ^^Thing).flag + ubyte @shared derp6 = (t.port as ^^Thing).flag as ubyte } }