1
0
mirror of https://github.com/KarolS/millfork.git synced 2024-05-29 04:41:30 +00:00

Enable pointers to functions with a word parameter (using trampolines on 6502)

This commit is contained in:
Karol Stasiak 2019-07-30 22:49:32 +02:00
parent 613ddcf9a4
commit 22b4776139
11 changed files with 150 additions and 29 deletions

View File

@ -60,3 +60,16 @@ Such functions should be marked as written in assembly and should have their par
* `<expression>` is an expression. It is equivalent to a function body of form `{ return <expression> }`.
The address of an non-macro function `f` is a constant `f.addr`.
Non-macro, non-interrupt functions which have max one parameter of size max 2 bytes
and return `void` or a value of size max 2 bytes,
can be accessed via a pointer.
void f() {}
function.void.to.void p = f.pointer
call(p)
The value of the pointer `f.pointer` may not be the same as the value of the function address `f.addr`.

View File

@ -282,7 +282,7 @@ and the result is a constant of either `byte` or `word` type, depending on situa
* `call`: calls a function via a pointer;
the first argument is the pointer to the function;
the second argument, if present, is the argument to the called function.
The function can have max one parameter, of size max 1 byte, and may return a value of size max 2 bytes.
The function can have max one parameter, of size max 2 bytes, and may return a value of size max 2 bytes.
You can't create typed pointers to other kinds of functions anyway.
If the pointed-to function returns a value, then the result of `call(...)` is the result of the function.
Using `call` on 6502 targets requires at least 4 bytes of zeropage pseudoregister.

View File

@ -77,7 +77,7 @@ Its actual value is defined using the feature `NULLPTR`, by default it's 0.
## Function pointers
For every type `A` of size 1 (or `void`) and every type `B` of size 1 or 2 (or `void`),
For every type `A` of size 1 or 2 (or `void`) and every type `B` of size 1 or 2 (or `void`),
there is a pointer type defined called `function.A.to.B`, which represents functions with a signature like this:
B function_name(A parameter)
@ -90,9 +90,12 @@ Examples:
i = call(p1)
function.byte.to.byte p2 = f2.pointer
i += call(p2, 7)
function.word.to.byte p3 = f3.pointer
i += call(p2, 7)
Using `call` on 6502 requires at least 4 bytes of zeropage pseudoregister.
The value of the pointer `f.pointer` may not be the same as the value of the function address `f.addr`.
## Boolean types

View File

@ -26,7 +26,7 @@
<Keywords name="Folders in comment, close"></Keywords>
<Keywords name="Keywords1">void bool byte sbyte ubyte array word farword pointer farpointer addr long word_be word_le long_be long_le file int8 int16 int24 int32 int40 int48 int56 int64 signed8 fast</Keywords>
<Keywords name="Keywords2">if else for return while do asm extern import segment break continue default alias enum struct union goto label</Keywords>
<Keywords name="Keywords3">defaultz petscii ascii scr petscr pet atascii atari bbc sinclair apple2 jis jisx iso_de iso_yu iso_no iso_dk iso_se iso_fi petsciiz asciiz scrz petscrz petz atasciiz atariz bbcz sinclairz apple2z jisz jisxz iso_dez iso_yuz iso_noz iso_dkz iso_sez iso_fiz until to downto parallelto static stack ref const volatile paralleluntil inline noinline macro register kernal_interrupt interrupt reentrant hi lo sin cos tan nonet align false true nullptr</Keywords>
<Keywords name="Keywords3">defaultz petscii ascii scr petscr pet atascii atari bbc sinclair apple2 jis jisx iso_de iso_yu iso_no iso_dk iso_se iso_fi petsciiz asciiz scrz petscrz petz atasciiz atariz bbcz sinclairz apple2z jisz jisxz iso_dez iso_yuz iso_noz iso_dkz iso_sez iso_fiz until to downto parallelto static stack ref const volatile paralleluntil inline noinline macro register kernal_interrupt interrupt reentrant hi lo sin cos tan call nonet align false true nullptr</Keywords>
<Keywords name="Keywords4">&quot;sta &quot; &quot;lda &quot; &quot;jmp &quot; &quot;bit &quot; &quot;eor &quot; &quot;adc &quot; &quot;sbc &quot; &quot;ora &quot; &quot;and &quot; &quot;ldx &quot; &quot;ldy &quot; &quot;stx &quot; &quot;sty &quot; &quot;tax&quot; &quot;tay&quot; &quot;tya&quot; &quot;txa&quot; &quot;txs&quot; &quot;tsx&quot; &quot;sei&quot; &quot;cli&quot; &quot;clv&quot; &quot;clc&quot; &quot;cld&quot; &quot;sed&quot; &quot;sec&quot; &quot;bra &quot; &quot;beq &quot; &quot;bne &quot; &quot;bmi &quot; &quot;bpl &quot; &quot;bcc &quot; &quot;bcs &quot; &quot;bvs &quot; bvc &quot; &quot;jsr &quot; rts&quot; &quot;rti&quot; &quot;brk&quot; &quot;rol&quot; &quot;ror&quot; &quot;asl&quot; &quot;lsr&quot; &quot;inc &quot; &quot;dec &quot; &quot;cmp &quot; &quot;cpx &quot; &quot;cpy &quot; inx iny dex dey pla pha plp hp phx plx phy ply &quot;stz &quot; &quot;ldz &quot; tza taz &quot;tsb &quot; &quot;trb &quot; ra txy tyx pld plb phb phd phk xce&#x000D;&#x000A;&#x000D;&#x000A;&quot;STA &quot; &quot;LDA &quot; &quot;JMP &quot; &quot;BIT &quot; &quot;EOR &quot; &quot;ADC &quot; &quot;SBC &quot; &quot;ORA &quot; &quot;AND &quot; &quot;LDX &quot; &quot;LDY &quot; &quot;STX &quot; &quot;STY &quot; &quot;TAX&quot; &quot;TAY&quot; &quot;TYA&quot; &quot;TXA&quot; &quot;TXS&quot; &quot;TSX&quot; &quot;SEI&quot; &quot;CLI&quot; &quot;CLV&quot; &quot;CLC&quot; &quot;CLD&quot; &quot;SED&quot; &quot;SEC&quot; &quot;BEQ &quot; &quot;BRA &quot; &quot;BNE &quot; &quot;BMI &quot; &quot;BPL &quot; &quot;BCC &quot; &quot;BCS &quot; &quot;BVS &quot; BVC &quot; &quot;JSR &quot; RTS&quot; &quot;RTI&quot; &quot;BRK&quot; &quot;ROL&quot; &quot;ROR&quot; &quot;ASL&quot; &quot;LSR&quot; &quot;INC &quot; &quot;DEC &quot; &quot;CMP &quot; &quot;CPX &quot; &quot;CPY &quot; INX INY DEX DEY PLA PHA PLP HP PHX PLX PHY PLY &quot;STZ &quot; &quot;LDZ &quot; TZA TAZ &quot;TSB &quot; &quot;TRB &quot; RA TXY TYX PLD PLB PHB PHD PHK XCE</Keywords>
<Keywords name="Keywords5">&quot;sbx &quot; &quot;isc &quot; &quot;dcp &quot; &quot;lax &quot; &quot;sax &quot; &quot;anc &quot; &quot;alr &quot; &quot;arr &quot; &quot;rra &quot; &quot;rla &quot; &quot;lxa &quot; &quot;ane &quot; &quot;xaa &quot;&#x000D;&#x000A;&quot;SBX &quot; &quot;ISC &quot; &quot;DCP &quot; &quot;LAX &quot; &quot;SAX &quot; &quot;ANC &quot; &quot;ALR &quot; &quot;ARR &quot; &quot;RRA &quot; &quot;RLA &quot; &quot;LXA &quot; &quot;ANE &quot; &quot;XAA &quot;</Keywords>
<Keywords name="Keywords6"></Keywords>

View File

@ -24,7 +24,7 @@ object EmptyParameterStoreRemoval extends AssemblyOptimization[AssemblyLine] {
case _ => None
}.toSet
val foreignVariables = f.environment.root.things.values.flatMap {
case other: NormalFunction =>
case other: NormalFunction if !other.name.endsWith(".trampoline") =>
val address = other.address match {
case Some(NumericConstant(addr, _)) => "$" + addr.toHexString
case _ => ""

View File

@ -128,17 +128,19 @@ object MosExpressionCompiler extends AbstractExpressionCompiler[AssemblyLine] {
}
def preserveRegisterIfNeeded(ctx: CompilationContext, register: MosRegister.Value, code: List[AssemblyLine]): List[AssemblyLine] = {
val state = register match {
case MosRegister.A => State.A
case MosRegister.X => State.X
case MosRegister.Y => State.Y
val states = register match {
case MosRegister.A => Seq(State.A)
case MosRegister.AX | MosRegister.XA => Seq(State.A, State.X)
case MosRegister.X => Seq(State.X)
case MosRegister.AY | MosRegister.YA => Seq(State.A, State.Y)
case MosRegister.Y => Seq(State.Y)
}
val cmos = ctx.options.flag(CompilationFlag.EmitCmosOpcodes)
if (AssemblyLine.treatment(code, state) != Treatment.Unchanged) {
if (states.exists(state => AssemblyLine.treatment(code, state) != Treatment.Unchanged)) {
register match {
case MosRegister.A => AssemblyLine.implied(PHA) +: fixTsx(code) :+ AssemblyLine.implied(PLA)
case MosRegister.X => if (cmos) {
case MosRegister.X | MosRegister.AX | MosRegister.XA => if (cmos) {
List(
AssemblyLine.implied(PHA),
AssemblyLine.implied(PHX),
@ -157,7 +159,7 @@ object MosExpressionCompiler extends AbstractExpressionCompiler[AssemblyLine] {
AssemblyLine.implied(PLA),
)
}
case MosRegister.Y => if (cmos) {
case MosRegister.Y | MosRegister.AY | MosRegister.YA => if (cmos) {
List(
AssemblyLine.implied(PHA),
AssemblyLine.implied(PHY),
@ -1179,11 +1181,17 @@ object MosExpressionCompiler extends AbstractExpressionCompiler[AssemblyLine] {
case List(fp, param) =>
getExpressionType(ctx, fp) match {
case FunctionPointerType(_, _, _, Some(pt), Some(v)) =>
if (pt.size != 1) {
if (pt.size > 2 || pt.size < 1) {
ctx.log.error("Invalid parameter type", param.position)
compile(ctx, fp, None, BranchSpec.None) ++ compile(ctx, param, None, BranchSpec.None)
} else if (getExpressionType(ctx, param).isAssignableTo(pt)) {
compileToA(ctx, param) ++ preserveRegisterIfNeeded(ctx, MosRegister.A, compileToZReg2(ctx, fp)) :+ AssemblyLine.absolute(JSR, env.get[ThingInMemory]("call"))
pt.size match {
case 1 =>
compileToA(ctx, param) ++ preserveRegisterIfNeeded(ctx, MosRegister.A, compileToZReg2(ctx, fp)) :+ AssemblyLine.absolute(JSR, env.get[ThingInMemory]("call"))
case 2 =>
compileToAX(ctx, param) ++ preserveRegisterIfNeeded(ctx, MosRegister.AX, compileToZReg2(ctx, fp)) :+ AssemblyLine.absolute(JSR, env.get[ThingInMemory]("call"))
}
} else {
ctx.log.error("Invalid parameter type", param.position)
compile(ctx, fp, None, BranchSpec.None) ++ compile(ctx, param, None, BranchSpec.None)

View File

@ -979,7 +979,13 @@ class Environment(val parent: Option[Environment], val prefix: String, val cpuFa
log.error(s"Non-macro function `$name` cannot have inlinable parameters", stmt.position)
}
val env = new Environment(Some(this), name + "$", cpuFamily, options)
val isTrampoline = stmt.name.endsWith(".trampoline")
val env = if (isTrampoline) {
// let's hope nothing goes wrong with this:
get[FunctionInMemory](stmt.name.stripSuffix(".trampoline")).environment
} else {
new Environment(Some(this), name + "$", cpuFamily, options)
}
stmt.params.foreach(p => env.registerParameter(p, options))
def params: ParamSignature = if (stmt.assembly) {
AssemblyParamSignature(stmt.params.map {
@ -1194,10 +1200,15 @@ class Environment(val parent: Option[Environment], val prefix: String, val cpuFa
addThing(ConstantThing(thing.name + ".rawaddr.lo", rawaddr.loByte, get[Type]("byte")), position)
thing match {
case f: FunctionInMemory if f.canBePointedTo =>
val typedPointer = RelativeVariable(thing.name + ".pointer", addr, getFunctionPointerType(f), zeropage = false, None, isVolatile = false)
val actualAddr = if (f.requiresTrampoline(options)) {
registerFunctionTrampoline(f).toAddress
} else {
addr
}
val typedPointer = RelativeVariable(thing.name + ".pointer", actualAddr, getFunctionPointerType(f), zeropage = false, None, isVolatile = false)
addThing(typedPointer, position)
addThing(RelativeVariable(thing.name + ".pointer.hi", addr + 1, b, zeropage = false, None, isVolatile = false), position)
addThing(RelativeVariable(thing.name + ".pointer.lo", addr, b, zeropage = false, None, isVolatile = false), position)
addThing(RelativeVariable(thing.name + ".pointer.hi", actualAddr + 1, b, zeropage = false, None, isVolatile = false), position)
addThing(RelativeVariable(thing.name + ".pointer.lo", actualAddr, b, zeropage = false, None, isVolatile = false), position)
case _ =>
}
} else {
@ -1218,14 +1229,51 @@ class Environment(val parent: Option[Environment], val prefix: String, val cpuFa
thing match {
case f: FunctionInMemory if f.canBePointedTo =>
val pointerType = getFunctionPointerType(f)
addThing(ConstantThing(thing.name + ".pointer", addr, pointerType), position)
addThing(ConstantThing(thing.name + ".pointer.hi", addr.hiByte, b), position)
addThing(ConstantThing(thing.name + ".pointer.lo", addr.loByte, b), position)
val actualAddr = if (f.requiresTrampoline(options)) {
registerFunctionTrampoline(f).toAddress
} else {
addr
}
addThing(ConstantThing(thing.name + ".pointer", actualAddr, pointerType), position)
addThing(ConstantThing(thing.name + ".pointer.hi", actualAddr.hiByte, b), position)
addThing(ConstantThing(thing.name + ".pointer.lo", actualAddr.loByte, b), position)
case _ =>
}
}
}
def registerFunctionTrampoline(function: FunctionInMemory): FunctionInMemory = {
options.platform.cpuFamily match {
case CpuFamily.M6502 =>
function.params match {
case NormalParamSignature(List(param)) =>
import Opcode._
import AddrMode._
val localNameForParam = param.name.stripPrefix(function.name + '$')
root.registerFunction(FunctionDeclarationStatement(
function.name + ".trampoline",
function.returnType.name,
List(ParameterDeclaration(param.typ.name, ByMosRegister(MosRegister.AX))),
Some(function.bank(options)),
None, None,
Some(List(
MosAssemblyStatement(STA, Absolute, VariableExpression(localNameForParam), Elidability.Volatile),
MosAssemblyStatement(STX, Absolute, VariableExpression(localNameForParam) #+# 1, Elidability.Volatile),
MosAssemblyStatement(JMP, Absolute, VariableExpression(function.name + ".addr"), Elidability.Elidable)
)),
isMacro = false,
inlinable = Some(false),
assembly = true,
interrupt = false,
kernalInterrupt = false,
reentrant = false
), options)
get[FunctionInMemory](function.name + ".trampoline")
}
case _ => function
}
}
def registerParameter(stmt: ParameterDeclaration, options: CompilationOptions): Unit = {
val typ = get[Type](stmt.typ)
val b = get[Type]("byte")

View File

@ -1,7 +1,7 @@
package millfork.env
import millfork.assembly.BranchingOpcodeMapping
import millfork.{CompilationFlag, CompilationOptions}
import millfork.{CompilationFlag, CompilationOptions, CpuFamily}
import millfork.node._
import millfork.output.{MemoryAlignment, NoAlignment}
@ -350,6 +350,8 @@ sealed trait MangledFunction extends CallableThing {
def interrupt: Boolean
def canBePointedTo: Boolean
def requiresTrampoline(compilationOptions: CompilationOptions): Boolean = false
}
case class EmptyFunction(name: String,
@ -380,6 +382,10 @@ sealed trait FunctionInMemory extends MangledFunction with ThingInMemory {
override def bank(compilationOptions: CompilationOptions): String =
declaredBank.getOrElse(compilationOptions.platform.defaultCodeBank)
override def canBePointedTo: Boolean = !interrupt && returnType.size <= 2 && params.canBePointedTo && name !="call"
override def requiresTrampoline(compilationOptions: CompilationOptions): Boolean = params.requireTrampoline(compilationOptions)
}
case class ExternFunction(name: String,
@ -395,8 +401,6 @@ case class ExternFunction(name: String,
override def zeropage: Boolean = false
override def isVolatile: Boolean = false
override def canBePointedTo: Boolean = !interrupt && returnType.size <= 2 && params.canBePointedTo && name !="call"
}
case class NormalFunction(name: String,
@ -418,8 +422,6 @@ case class NormalFunction(name: String,
override def zeropage: Boolean = false
override def isVolatile: Boolean = false
override def canBePointedTo: Boolean = !interrupt && returnType.size <= 2 && params.canBePointedTo && name !="call"
}
case class ConstantThing(name: String, value: Constant, typ: Type) extends TypedThing with VariableLikeThing with IndexableThing {
@ -436,6 +438,8 @@ trait ParamSignature {
def length: Int
def canBePointedTo: Boolean
def requireTrampoline(compilationOptions: CompilationOptions): Boolean
}
case class NormalParamSignature(params: List[VariableInMemory]) extends ParamSignature {
@ -443,7 +447,13 @@ case class NormalParamSignature(params: List[VariableInMemory]) extends ParamSig
override def types: List[Type] = params.map(_.typ)
def canBePointedTo: Boolean = params.size <= 1 && params.forall(_.typ.size.<=(1))
def canBePointedTo: Boolean = params.size <= 1 && params.forall(_.typ.size.<=(2))
def requireTrampoline(compilationOptions: CompilationOptions): Boolean = compilationOptions.platform.cpuFamily match {
case CpuFamily.M6502 => params.exists(_.typ.size.>=(2))
case _ => false
}
}
sealed trait ParamPassingConvention {
@ -507,6 +517,9 @@ case class AssemblyParamSignature(params: List[AssemblyParam]) extends ParamSign
override def types: List[Type] = params.map(_.typ)
def canBePointedTo: Boolean = params.size <= 1 && params.forall(_.canBePointedTo)
override def requireTrampoline(compilationOptions: CompilationOptions): Boolean =
false // all pointable functions with this kind of signature by definition use the pure register-cased parameter passing convention
}
case class EmptyFunctionParamSignature(paramType: Type) extends ParamSignature {
@ -515,4 +528,6 @@ case class EmptyFunctionParamSignature(paramType: Type) extends ParamSignature {
override def types: List[Type] = List(paramType)
def canBePointedTo: Boolean = false
}
override def requireTrampoline(compilationOptions: CompilationOptions): Boolean = false
}

View File

@ -47,6 +47,7 @@ abstract class CallGraph(program: Program, log: Logger) {
aliases += name -> target
case f: FunctionDeclarationStatement =>
allFunctions += f.name
allFunctions += f.name + ".trampoline" // TODO: ???
if (f.address.isDefined || f.interrupt) entryPoints += f.name
f.statements.getOrElse(Nil).foreach(s => this.add(Some(f.name), Nil, s))
case s: Statement =>
@ -59,7 +60,13 @@ abstract class CallGraph(program: Program, log: Logger) {
case s: SumExpression =>
s.expressions.foreach(expr => add(currentFunction, callingFunctions, expr._2))
case x: VariableExpression =>
val varName = x.name.stripSuffix(".hi").stripSuffix(".lo").stripSuffix(".addr").stripSuffix(".pointer")
val varName0 = x.name.stripSuffix(".hi").stripSuffix(".lo")
if (varName0.endsWith(".pointer")) {
val trampolineName = varName0.stripSuffix(".pointer") + ".trampoline"
everCalledFunctions += trampolineName
entryPoints += trampolineName
}
val varName = varName0.stripSuffix(".addr").stripSuffix(".pointer")
everCalledFunctions += varName
entryPoints += varName // TODO: figure out how to interpret pointed-to functions
case i: IndexedExpression =>
@ -89,6 +96,8 @@ abstract class CallGraph(program: Program, log: Logger) {
case (a,b) => aliases.get(b).map(a -> _)
}
callEdges ++= everCalledFunctions.filter(_.endsWith(".trampoline")).map(t => t -> t.stripSuffix(".trampoline"))
var changed = true
while (changed) {
changed = false

View File

@ -273,7 +273,9 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program
}
}
val unusedRuntimeObjects = Set("__mul_u8u8u8", "__constant8", "identity$", "__mul_u16u8u16", "__divmod_u16u8u16u8", "__mod_u8u8u8u8", "__div_u8u8u8u8").filterNot(name =>{
val objectsThatMayBeUnused = Set("__mul_u8u8u8", "__constant8", "identity$", "__mul_u16u8u16", "__divmod_u16u8u16u8", "__mod_u8u8u8u8", "__div_u8u8u8u8") ++
compiledFunctions.keySet.filter(_.endsWith(".trampoline"))
val unusedRuntimeObjects = objectsThatMayBeUnused.filterNot(name =>{
compiledFunctions.exists{
case (fname, compiled) => fname != name && (compiled match {
case f:NormalCompiledFunction[_] => f.code.exists(_.refersTo(name))

View File

@ -99,4 +99,27 @@ class FunctionPointerSuite extends FunSuite with Matchers with AppendedClues{
|""".stripMargin)
}
test("Function pointers 3") {
EmuUnoptimizedCrossPlatformRun (Cpu.Mos, Cpu.Z80)(
"""
| const byte COUNT = 128
| array(word) output0[COUNT] @$c000
| noinline void fill(function.word.to.word f) {
| byte i
| for i,0,until,COUNT {
| output0[i] = call(f, word(i))
| }
| }
| word id(word x) = x
| void main() {
| fill(id.pointer)
| }
|
""".stripMargin) { m =>
for (i <- 0 until 0x80) {
m.readByte(0xc000 + i * 2) should equal(i) withClue ("id " + i)
}
}
}
}