From c599db0068a8a3eafc53360edc59adb98a40bd32 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Thu, 1 Feb 2018 00:22:53 +0100 Subject: [PATCH] Inline assembly improvements --- doc/lang/assembly.md | 7 ++-- include/stdlib.mfk | 5 ++- .../scala/millfork/compiler/MfCompiler.scala | 36 ++++++++++++++----- .../test/InlineAssemblyFunctionsSuite.scala | 30 +++++++++++++--- 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/doc/lang/assembly.md b/doc/lang/assembly.md index 98b7aaae..be5ce235 100644 --- a/doc/lang/assembly.md +++ b/doc/lang/assembly.md @@ -58,11 +58,12 @@ Assembly functions can be declared as `inline` or not. An inline assembly function is inserted into the calling function like an inline assembly block, and therefore usually it shouldn't end with `RTS` or `RTI`. -The return type on inline functions has to be `void`. A non-inline assembly function should end with `RTS`, `JMP` or `RTI` as appropriate, or it should be an external function. -Their return type can be any valid return type, like for Millfork functions. + +For both inline and non-inline assembly functions, +the return type can be any valid return type, like for Millfork functions. If the size of the return type is one byte, then the result is passed via the accumulator. If the size of the return type is two bytes, @@ -103,6 +104,8 @@ Non-inline functions can only have their parameters passed via registers: * `word xa`, `word ax`, `word ay`, `word ya`, `word xy`, `word yx`: a 2-byte word byte passed via given two CPU registers, with the high byte passed through the first register and the low byte passed through the second register +Inline assembly functions can have maximum one parameter passed via a register. + ### External functions An external function should be declared with a defined memory address diff --git a/include/stdlib.mfk b/include/stdlib.mfk index 41327936..7eb85322 100644 --- a/include/stdlib.mfk +++ b/include/stdlib.mfk @@ -4,13 +4,12 @@ word nmi_routine_addr @$FFFA word reset_routine_addr @$FFFC word irq_routine_addr @$FFFE -inline asm void poke(word const addr, byte const value) { - ?LDA #value +inline asm void poke(word const addr, byte a) { STA addr } inline asm byte peek(word const addr) { - LDA addr + ?LDA addr } inline asm void disable_irq() { diff --git a/src/main/scala/millfork/compiler/MfCompiler.scala b/src/main/scala/millfork/compiler/MfCompiler.scala index 58a76557..62f561d4 100644 --- a/src/main/scala/millfork/compiler/MfCompiler.scala +++ b/src/main/scala/millfork/compiler/MfCompiler.scala @@ -1105,14 +1105,14 @@ object MfCompiler { } lookupFunction(ctx, f) match { case function: InlinedFunction => - inlineFunction(function, params, Some(ctx)).map { + val (paramPreparation, statements) = inlineFunction(ctx, function, params) + paramPreparation ++ statements.map { case AssemblyStatement(opcode, addrMode, expression, elidable) => val param = env.evalForAsm(expression).getOrElse { ErrorReporting.error("Inlining failed due to non-constant things", expression.position) Constant.Zero } AssemblyLine(opcode, addrMode, param, elidable) - } case function: EmptyFunction => ??? // TODO: type conversion? @@ -1344,18 +1344,20 @@ object MfCompiler { SequenceChunk(statements.map(s => compile(ctx, s))) } - def inlineFunction(i: InlinedFunction, params: List[Expression], cc: Option[CompilationContext]): List[ExecutableStatement] = { + def inlineFunction(ctx: CompilationContext, i: InlinedFunction, params: List[Expression]): (List[AssemblyLine], List[ExecutableStatement]) = { + var paramPreparation = List[AssemblyLine]() var actualCode = i.code i.params match { case AssemblyParamSignature(assParams) => + var hadRegisterParam = false assParams.zip(params).foreach { case (AssemblyParam(typ, Placeholder(ph, phType), AssemblyParameterPassingBehaviour.ByReference), actualParam) => actualParam match { case VariableExpression(vname) => - cc.foreach(_.env.get[ThingInMemory](vname)) + ctx.env.get[ThingInMemory](vname) case l: LhsExpression => // TODO: ?? - cc.foreach(c => compileByteStorage(c, Register.A, l)) + compileByteStorage(ctx, Register.A, l) case _ => ErrorReporting.error("A non-assignable expression was passed to an inlineable function as a `ref` parameter", actualParam.position) } @@ -1365,12 +1367,18 @@ object MfCompiler { case x => x } case (AssemblyParam(typ, Placeholder(ph, phType), AssemblyParameterPassingBehaviour.ByConstant), actualParam) => - cc.foreach(_.env.eval(actualParam).getOrElse(Constant.error("Non-constant expression was passed to an inlineable function as a `const` parameter", actualParam.position))) + ctx.env.eval(actualParam).getOrElse(Constant.error("Non-constant expression was passed to an inlineable function as a `const` parameter", actualParam.position)) actualCode = actualCode.map { case a@AssemblyStatement(_, _, expr, _) => a.copy(expression = expr.replaceVariable(ph, actualParam)) case x => x } + case (AssemblyParam(typ, v@RegisterVariable(register, _), AssemblyParameterPassingBehaviour.Copy), actualParam) => + if (hadRegisterParam) { + ErrorReporting.error("Only one inline assembly function parameter can be passed via a register") + } + hadRegisterParam = true + paramPreparation = compile(ctx, actualParam, Some(typ, v), BranchSpec.None) case (AssemblyParam(_, _, AssemblyParameterPassingBehaviour.Copy), actualParam) => ??? case (_, actualParam) => @@ -1378,7 +1386,18 @@ object MfCompiler { case NormalParamSignature(Nil) => case NormalParamSignature(normalParams) => ??? } - actualCode + // fix local labels: + // TODO: do it even if the labels are in an inline assembly block inside a Millfork function + val localLabels = actualCode.flatMap{ + case AssemblyStatement(LABEL, _, VariableExpression(l), _) => Some(l) + case _ => None + }.toSet + val labelPrefix = nextLabel("il") + paramPreparation -> actualCode.map{ + case s@AssemblyStatement(_, _, VariableExpression(v), _) if localLabels(v) => + s.copy(expression = VariableExpression(labelPrefix + v)) + case s => s + } } def stackPointerFixAtBeginning(ctx: CompilationContext): List[AssemblyLine] = { @@ -1483,7 +1502,8 @@ object MfCompiler { case ExpressionStatement(e@FunctionCallExpression(name, params)) => env.lookupFunction(name, params.map(p => getExpressionType(ctx, p) -> p)) match { case Some(i: InlinedFunction) => - compile(ctx, inlineFunction(i, params, Some(ctx))) + val (paramPreparation, inlinedStatements) = inlineFunction(ctx, i, params) + SequenceChunk(List(LinearChunk(paramPreparation), compile(ctx, inlinedStatements))) case _ => LinearChunk(compile(ctx, e, None, NoBranching)) } diff --git a/src/test/scala/millfork/test/InlineAssemblyFunctionsSuite.scala b/src/test/scala/millfork/test/InlineAssemblyFunctionsSuite.scala index 2d62fe33..c6c33447 100644 --- a/src/test/scala/millfork/test/InlineAssemblyFunctionsSuite.scala +++ b/src/test/scala/millfork/test/InlineAssemblyFunctionsSuite.scala @@ -13,8 +13,7 @@ class InlineAssemblyFunctionsSuite extends FunSuite with Matchers { test("Poke test 1") { EmuBenchmarkRun( """ - | inline asm void poke(word ref addr, byte const value) { - | ?LDA #value + | inline asm void poke(word ref addr, byte a) { | STA addr | } | @@ -46,8 +45,7 @@ class InlineAssemblyFunctionsSuite extends FunSuite with Matchers { test("Poke test 2") { EmuBenchmarkRun( """ - | inline asm void poke(word const addr, byte const value) { - | ?LDA #value + | inline asm void poke(word const addr, byte a) { | STA addr | } | @@ -60,6 +58,7 @@ class InlineAssemblyFunctionsSuite extends FunSuite with Matchers { m.readByte(0xc000) should equal(5) } } + test("Peek test 2") { EmuBenchmarkRun( """ @@ -77,4 +76,27 @@ class InlineAssemblyFunctionsSuite extends FunSuite with Matchers { m.readByte(0xc000) should equal(5) } } + + test("Labels test") { + EmuBenchmarkRun( + """ + | inline asm void doNothing () { + | JMP label + | label: + | } + | + | byte output @$c000 + | void main () { + | output = 0 + | doNothing() + | output += 1 + | doNothing() + | output += 1 + | doNothing() + | output += 1 + | } + """.stripMargin) { m => + m.readByte(0xc000) should equal(3) + } + } }