Merge branch 'master' into new-codegens

# Conflicts:
#	codeGenCpu6502/src/prog8/codegen/cpu6502/assignment/PointerAssignmentsGen.kt
#	compiler/test/TestPointers.kt
#	docs/source/todo.rst
#	examples/test.p8
This commit is contained in:
Irmen de Jong committed 2026-08-21 19:14:29 +02:00
commit 4b320013cf
11 files changed
+323 -118

No files matched your search

+2 -2
View File
@@ -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
@@ -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}")
}
}
@@ -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""")
}
@@ -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 {
@@ -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) {
@@ -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
+154
View File
@@ -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<Assignment>()
.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<Assignment>()
.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
}
})
@@ -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 {
+2 -2
View File
@@ -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 <name> = <target>`` and then you can use ``<name>`` as if it were ``<target>``.
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::
+1 -2
View File
@@ -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.
+15 -58
View File
@@ -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
}
}