From 2583f280a82b97518055f75162df645077bc41c9 Mon Sep 17 00:00:00 2001 From: Karol Stasiak Date: Thu, 6 Jun 2019 01:17:34 +0200 Subject: [PATCH] Unsigned byte division by non-constant --- CHANGELOG.md | 2 +- docs/lang/operators.md | 5 +- include/i80_math.mfk | 33 ++++++++++++ include/zp_reg.mfk | 25 +++++++++ .../opt/ZeropageRegisterOptimizations.scala | 2 + .../assembly/z80/opt/CoarseFlowAnalyzer.scala | 2 +- .../z80/opt/ReverseFlowAnalyzer.scala | 6 +-- .../millfork/compiler/mos/BuiltIns.scala | 12 ++--- .../compiler/mos/PseudoregisterBuiltIns.scala | 25 +++++++-- .../compiler/z80/Z80ExpressionCompiler.scala | 6 ++- .../millfork/compiler/z80/Z80Multiply.scala | 48 +++++++++++++---- .../millfork/node/opt/UnusedFunctions.scala | 10 ++++ .../millfork/output/AbstractAssembler.scala | 2 +- .../scala/millfork/test/ByteMathSuite.scala | 53 +++++++++++++++++-- 14 files changed, 195 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a11fd391..4cd1d576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ * Added structs and unions. -* Added unsigned byte division and modulo by a constant. +* Added unsigned byte division and modulo. * Pointers can now be allocated anywhere. diff --git a/docs/lang/operators.md b/docs/lang/operators.md index ec4d76c4..833c16c0 100644 --- a/docs/lang/operators.md +++ b/docs/lang/operators.md @@ -52,9 +52,6 @@ In the descriptions below, arguments to the operators are explained as follows: * `constant` means a compile-time constant -* `simpleconstant` means a compile-time constant evaluable at the first compilation pass -(eg. a literal or a combination of literals, not an undefined address) - * `simple` means either: a constant, a non-stack variable, a pointer indexed with a constant, a pointer indexed with a non-stack variable, an array indexed with a constant, an array indexed with a non-stack variable, @@ -96,7 +93,7 @@ TODO * `/`, `%%`: unsigned division and unsigned modulo -`byte / simpleconstant byte` +`byte / byte` `constant word / constant word` `constant long / constant long` diff --git a/include/i80_math.mfk b/include/i80_math.mfk index 803c9ab1..698dc586 100644 --- a/include/i80_math.mfk +++ b/include/i80_math.mfk @@ -50,6 +50,39 @@ noinline asm byte __mul_u8u8u8() { #endif +noinline asm void __divmod_u16u8u16u8() { + ? XOR A + ? LD B, 16 +__divmod_u16u8u16u8_loop: + ? ADD HL,HL + ? RLA +#if CPUFEATURE_Z80 || CPUFEATURE_GAMEBOY + ? JR C, __divmod_u16u8u16u8_overflow +#else + ? JP C, __divmod_u16u8u16u8_overflow +#endif + ? CP D +#if CPUFEATURE_Z80 || CPUFEATURE_GAMEBOY + ? JR C, __divmod_u16u8u16u8_skip +#else + ? JP C, __divmod_u16u8u16u8_skip +#endif +__divmod_u16u8u16u8_overflow: + ? SUB D + ? INC L +__divmod_u16u8u16u8_skip: +#if CPUFEATURE_Z80 + ? DJNZ __divmod_u16u8u16u8_loop +#elseif CPUFEATURE_GAMEBOY + ? DEC B + ? JR NZ, __divmod_u16u8u16u8_loop +#else + ? DEC B + ? JP NZ, __divmod_u16u8u16u8_loop +#endif + ? RET +} + inline asm word __mul_u16u8u16() { ? LD HL,0 ? LD B,8 diff --git a/include/zp_reg.mfk b/include/zp_reg.mfk index 59130f0c..56b8807b 100644 --- a/include/zp_reg.mfk +++ b/include/zp_reg.mfk @@ -18,6 +18,31 @@ __mul_u8u8u8_start: ? RTS } +// divide __reg[0]/__reg[1] + +noinline asm byte __mod_u8u8u8u8() { + ? LDA #0 + ? LDX #7 + ? CLC +__divmod_u8u8u8u8_start: + ? ROL __reg + ? ROL + ? CMP __reg+1 + ? BCC __divmod_u8u8u8u8_skip + ? SBC __reg+1 +__divmod_u8u8u8u8_skip: + ? DEX + ? BPL __divmod_u8u8u8u8_start + ? ROL __reg + ? RTS +} + +asm byte __div_u8u8u8u8() { + ? JSR __mod_u8u8u8u8 + ? LDA __reg + ? RTS +} + #if ZPREG_SIZE >= 3 noinline asm word __mul_u16u8u16() { diff --git a/src/main/scala/millfork/assembly/mos/opt/ZeropageRegisterOptimizations.scala b/src/main/scala/millfork/assembly/mos/opt/ZeropageRegisterOptimizations.scala index 7d492aef..71e6e5bd 100644 --- a/src/main/scala/millfork/assembly/mos/opt/ZeropageRegisterOptimizations.scala +++ b/src/main/scala/millfork/assembly/mos/opt/ZeropageRegisterOptimizations.scala @@ -13,6 +13,8 @@ object ZeropageRegisterOptimizations { val functionsThatUsePseudoregisterAsInput: Map[String, Set[Int]] = Map( "__mul_u8u8u8" -> Set(0, 1), + "__mod_u8u8u8u8" -> Set(0, 1), + "__div_u8u8u8u8" -> Set(0, 1), "__mul_u16u8u16" -> Set(0, 1, 2), "__adc_decimal" -> Set(2, 3), "__sbc_decimal" -> Set(2, 3), diff --git a/src/main/scala/millfork/assembly/z80/opt/CoarseFlowAnalyzer.scala b/src/main/scala/millfork/assembly/z80/opt/CoarseFlowAnalyzer.scala index 1e454e2a..ebffec8e 100644 --- a/src/main/scala/millfork/assembly/z80/opt/CoarseFlowAnalyzer.scala +++ b/src/main/scala/millfork/assembly/z80/opt/CoarseFlowAnalyzer.scala @@ -25,7 +25,7 @@ object CoarseFlowAnalyzer { val preservesB: Set[String] = Set("__mul_u8u8u8") val preservesC: Set[String] = if (z80) Set("__mul_u8u8u8") else Set() val preservesD: Set[String] = Set() - val preservesE: Set[String] = Set() + val preservesE: Set[String] = Set("__divmod_u16u8u16u8") val preservesH: Set[String] = Set("__mul_u8u8u8") val preservesL: Set[String] = Set("__mul_u8u8u8") diff --git a/src/main/scala/millfork/assembly/z80/opt/ReverseFlowAnalyzer.scala b/src/main/scala/millfork/assembly/z80/opt/ReverseFlowAnalyzer.scala index cc4f8948..076a5ca4 100644 --- a/src/main/scala/millfork/assembly/z80/opt/ReverseFlowAnalyzer.scala +++ b/src/main/scala/millfork/assembly/z80/opt/ReverseFlowAnalyzer.scala @@ -186,10 +186,10 @@ object ReverseFlowAnalyzer { val readsA = Set("__mul_u8u8u8", "__mul_u16u8u16") val readsB = Set("") val readsC = Set("") - val readsD = Set("__mul_u8u8u8","__mul_u16u8u16") + val readsD = Set("__mul_u8u8u8","__mul_u16u8u16", "__divmod_u16u8u16u8") val readsE = Set("__mul_u16u8u16") - val readsH = Set("") - val readsL = Set("") + val readsH = Set("__divmod_u16u8u16u8") + val readsL = Set("__divmod_u16u8u16u8") //noinspection RedundantNewCaseClass def analyze(f: NormalFunction, code: List[ZLine]): List[CpuImportance] = { diff --git a/src/main/scala/millfork/compiler/mos/BuiltIns.scala b/src/main/scala/millfork/compiler/mos/BuiltIns.scala index 0ef713ec..b2fb8878 100644 --- a/src/main/scala/millfork/compiler/mos/BuiltIns.scala +++ b/src/main/scala/millfork/compiler/mos/BuiltIns.scala @@ -1028,12 +1028,12 @@ object BuiltIns { } else { compileUnsignedByteDivision(ctx, p, qq.toInt, modulo) } - case Some(_) => - ctx.log.error("Unsigned division by unknown constant", q.position) - Nil - case None => - ctx.log.error("Unsigned division by a variable expression", q.position) - Nil + case _ => + if (modulo) { + PseudoregisterBuiltIns.compileUnsignedByteModulo(ctx, Some(p), q, storeInRegLo = false) + } else { + PseudoregisterBuiltIns.compileUnsignedByteDivision(ctx, Some(p), q, storeInRegLo = false) + } } } def compileUnsignedByteDivision(ctx: CompilationContext, p: Expression, q: Int, modulo: Boolean): List[AssemblyLine] = { diff --git a/src/main/scala/millfork/compiler/mos/PseudoregisterBuiltIns.scala b/src/main/scala/millfork/compiler/mos/PseudoregisterBuiltIns.scala index ab7c8d5d..274bac0a 100644 --- a/src/main/scala/millfork/compiler/mos/PseudoregisterBuiltIns.scala +++ b/src/main/scala/millfork/compiler/mos/PseudoregisterBuiltIns.scala @@ -338,7 +338,14 @@ object PseudoregisterBuiltIns { } } - def compileByteMultiplication(ctx: CompilationContext, param1OrRegister: Option[Expression], param2: Expression, storeInRegLo: Boolean): List[AssemblyLine] = { + def compileByteMultiplication(ctx: CompilationContext, param1OrRegister: Option[Expression], param2: Expression, storeInRegLo: Boolean): List[AssemblyLine] = + compileByteMultiplicationOrDivision(ctx, param1OrRegister, param2, storeInRegLo, "__mul_u8u8u8", commutative = true) + def compileUnsignedByteDivision(ctx: CompilationContext, param1OrRegister: Option[Expression], param2: Expression, storeInRegLo: Boolean): List[AssemblyLine] = + compileByteMultiplicationOrDivision(ctx, param1OrRegister, param2, storeInRegLo, "__div_u8u8u8u8", commutative = false) + def compileUnsignedByteModulo(ctx: CompilationContext, param1OrRegister: Option[Expression], param2: Expression, storeInRegLo: Boolean): List[AssemblyLine] = + compileByteMultiplicationOrDivision(ctx, param1OrRegister, param2, storeInRegLo, "__mod_u8u8u8u8", commutative = false) + + def compileByteMultiplicationOrDivision(ctx: CompilationContext, param1OrRegister: Option[Expression], param2: Expression, storeInRegLo: Boolean, functionName: String, commutative: Boolean): List[AssemblyLine] = { if (ctx.options.zpRegisterSize < 2) { ctx.log.error("Variable byte multiplication requires the zeropage pseudoregister", param1OrRegister.flatMap(_.position)) return Nil @@ -352,10 +359,16 @@ object PseudoregisterBuiltIns { val code2 = MosExpressionCompiler.compile(ctx, param2, Some(b -> RegisterVariable(MosRegister.A, b)), BranchSpec.None) if (!usesRegLo(code2)) { code1 ++ List(AssemblyLine.zeropage(STA, reg)) ++ code2 ++ List(AssemblyLine.zeropage(STA, reg, 1)) + } else if (!commutative) { + code2 ++ List(AssemblyLine.implied(PHA)) ++ MosExpressionCompiler.fixTsx(code1) ++ List( + AssemblyLine.zeropage(STA, reg), + AssemblyLine.implied(PLA), + AssemblyLine.zeropage(STA, reg, 1) + ) } else if (!usesRegLo(code1)) { code2 ++ List(AssemblyLine.zeropage(STA, reg)) ++ code1 ++ List(AssemblyLine.zeropage(STA, reg, 1)) } else { - code1 ++ List(AssemblyLine.implied(PHA)) ++ code2 ++ List( + code1 ++ List(AssemblyLine.implied(PHA)) ++ MosExpressionCompiler.fixTsx(code2) ++ List( AssemblyLine.zeropage(STA, reg), AssemblyLine.implied(PLA), AssemblyLine.zeropage(STA, reg, 1) @@ -365,6 +378,12 @@ object PseudoregisterBuiltIns { val code2 = MosExpressionCompiler.compile(ctx, param2, Some(b -> RegisterVariable(MosRegister.A, b)), BranchSpec.None) if (!usesRegLo(code2)) { List(AssemblyLine.zeropage(STA, reg)) ++ code2 ++ List(AssemblyLine.zeropage(STA, reg, 1)) + } else if (!commutative) { + List(AssemblyLine.implied(PHA)) ++ MosExpressionCompiler.fixTsx(code2) ++ List( + AssemblyLine.zeropage(STA, reg, 1), + AssemblyLine.implied(PLA), + AssemblyLine.zeropage(STA, reg) + ) } else if (!usesRegHi(code2)) { List(AssemblyLine.zeropage(STA, reg, 1)) ++ code2 ++ List(AssemblyLine.zeropage(STA, reg)) } else { @@ -375,7 +394,7 @@ object PseudoregisterBuiltIns { ) } } - val calculate = AssemblyLine.absoluteOrLongAbsolute(JSR, ctx.env.get[FunctionInMemory]("__mul_u8u8u8"), ctx.options) :: + val calculate = AssemblyLine.absoluteOrLongAbsolute(JSR, ctx.env.get[FunctionInMemory](functionName), ctx.options) :: (if (storeInRegLo) List(AssemblyLine.zeropage(STA, reg)) else Nil) load ++ calculate } diff --git a/src/main/scala/millfork/compiler/z80/Z80ExpressionCompiler.scala b/src/main/scala/millfork/compiler/z80/Z80ExpressionCompiler.scala index 541162f5..5c8db8a0 100644 --- a/src/main/scala/millfork/compiler/z80/Z80ExpressionCompiler.scala +++ b/src/main/scala/millfork/compiler/z80/Z80ExpressionCompiler.scala @@ -886,8 +886,10 @@ object Z80ExpressionCompiler extends AbstractExpressionCompiler[ZLine] { size match { case 1 => calculateAddressToAppropriatePointer(ctx, l, forWriting = true) match { + case Some((LocalVariableAddressViaHL, List(ZLine0(LD_16, TwoRegisters(ZRegister.HL, ZRegister.IMM_16), addr)))) => + Z80Multiply.compileUnsignedByteDivision(ctx, Right(l), r, f.functionName == "%%=") :+ ZLine.ldAbs8(addr, ZRegister.A) case Some((lvo, code)) => - code ++ (Z80Multiply.compileUnsignedByteDivision(ctx, l, r, f.functionName == "%%=") :+ ZLine.ld8(lvo, ZRegister.A)) + code ++ (stashHLIfChanged(ctx, Z80Multiply.compileUnsignedByteDivision(ctx, Left(lvo), r, f.functionName == "%%=")) :+ ZLine.ld8(lvo, ZRegister.A)) case None => ctx.log.error("Invalid left-hand side", l.position) Nil @@ -898,7 +900,7 @@ object Z80ExpressionCompiler extends AbstractExpressionCompiler[ZLine] { val (l, r, size) = assertArithmeticAssignmentLike(ctx, params) size match { case 1 => - targetifyA(ctx, target, Z80Multiply.compileUnsignedByteDivision(ctx, l, r, f.functionName == "%%"), false) + targetifyA(ctx, target, Z80Multiply.compileUnsignedByteDivision(ctx, Right(l), r, f.functionName == "%%"), false) } case "*'=" => assertAllArithmeticBytes("Long multiplication not supported", ctx, params) diff --git a/src/main/scala/millfork/compiler/z80/Z80Multiply.scala b/src/main/scala/millfork/compiler/z80/Z80Multiply.scala index 8b559ae5..d41d59f0 100644 --- a/src/main/scala/millfork/compiler/z80/Z80Multiply.scala +++ b/src/main/scala/millfork/compiler/z80/Z80Multiply.scala @@ -106,7 +106,7 @@ object Z80Multiply { /** * Calculate A = p / q or A = p %% q */ - def compileUnsignedByteDivision(ctx: CompilationContext, p: LhsExpression, q: Expression, modulo: Boolean): List[ZLine] = { + def compileUnsignedByteDivision(ctx: CompilationContext, p: Either[LocalVariableAddressOperand, LhsExpression], q: Expression, modulo: Boolean): List[ZLine] = { ctx.env.eval(q) match { case Some(NumericConstant(qq, _)) => if (qq < 0) { @@ -116,27 +116,53 @@ object Z80Multiply { ctx.log.error("Unsigned division by zero", q.position) Nil } else if (qq > 255) { - if (modulo) Z80ExpressionCompiler.compileToA(ctx, p) - else List(ZLine.ldImm8(ZRegister.A, 0)) + if (modulo) { + p match { + case Right(pp) => Z80ExpressionCompiler.compileToA(ctx, pp) + case Left(LocalVariableAddressViaHL) => List(ZLine.ld8(ZRegister.A, ZRegister.MEM_HL)) + case Left(LocalVariableAddressViaIX(offset)) => List(ZLine.ldViaIx(ZRegister.A, offset)) + case Left(LocalVariableAddressViaIY(offset)) => List(ZLine.ldViaIy(ZRegister.A, offset)) + } + } else List(ZLine.ldImm8(ZRegister.A, 0)) } else { compileUnsignedByteDivisionImpl(ctx, p, qq.toInt, modulo) } - case Some(_) => - ctx.log.error("Unsigned division by unknown constant", q.position) - Nil - case None => - ctx.log.error("Unsigned division by a variable expression", q.position) - Nil + case _ => + val pb = p match { + case Right(pp) => Z80ExpressionCompiler.compileToHL(ctx, pp) + case Left(LocalVariableAddressViaHL) => List(ZLine.ld8(ZRegister.L, ZRegister.MEM_HL), ZLine.ldImm8(ZRegister.H, 0)) + case Left(LocalVariableAddressViaIX(offset)) => List(ZLine.ldViaIx(ZRegister.L, offset), ZLine.ldImm8(ZRegister.H, 0)) + case Left(LocalVariableAddressViaIY(offset)) => List(ZLine.ldViaIy(ZRegister.L, offset), ZLine.ldImm8(ZRegister.H, 0)) + } + val qb = Z80ExpressionCompiler.compileToA(ctx, q) + val load = if (qb.exists(Z80ExpressionCompiler.changesHL)) { + pb ++ Z80ExpressionCompiler.stashHLIfChanged(ctx, qb) + } else if (pb.exists(Z80ExpressionCompiler.changesDE)) { + qb ++ List(ZLine.ld8(ZRegister.D, ZRegister.A)) ++ Z80ExpressionCompiler.stashDEIfChanged(ctx, qb) + } else { + pb ++ qb ++ List(ZLine.ld8(ZRegister.D, ZRegister.A)) + } + val call = ZLine(ZOpcode.CALL, NoRegisters, ctx.env.get[FunctionInMemory]("__divmod_u16u8u16u8").toAddress) + if (modulo) { + load :+ call + } else { + load ++ List(call, ZLine.ld8(ZRegister.A, ZRegister.L)) + } } } /** * Calculate A = p / q or A = p %% q */ - def compileUnsignedByteDivisionImpl(ctx: CompilationContext, p: LhsExpression, q: Int, modulo: Boolean): List[ZLine] = { + def compileUnsignedByteDivisionImpl(ctx: CompilationContext, p: Either[LocalVariableAddressOperand, LhsExpression], q: Int, modulo: Boolean): List[ZLine] = { import ZRegister._ import ZOpcode._ val result = ListBuffer[ZLine]() - result ++= Z80ExpressionCompiler.compileToA(ctx, p) + result ++= (p match { + case Right(pp) => Z80ExpressionCompiler.compileToA(ctx, pp) + case Left(LocalVariableAddressViaHL) => List(ZLine.ld8(ZRegister.A, ZRegister.MEM_HL)) + case Left(LocalVariableAddressViaIX(offset)) => List(ZLine.ldViaIx(ZRegister.A, offset)) + case Left(LocalVariableAddressViaIY(offset)) => List(ZLine.ldViaIy(ZRegister.A, offset)) + }) result += ZLine.ldImm8(E, 0) for (i <- 7.to(0, -1)) { diff --git a/src/main/scala/millfork/node/opt/UnusedFunctions.scala b/src/main/scala/millfork/node/opt/UnusedFunctions.scala index aa1c5345..72637b34 100644 --- a/src/main/scala/millfork/node/opt/UnusedFunctions.scala +++ b/src/main/scala/millfork/node/opt/UnusedFunctions.scala @@ -15,6 +15,16 @@ object UnusedFunctions extends NodeOptimization { ("*", 3, "__mul_u16u8u16"), ("*=", 2, "__mul_u8u8u8"), ("*=", 2, "__mul_u16u8u16"), + ("/=", 0, "__divmod_u16u8u16u8"), + ("/", 0, "__divmod_u16u8u16u8"), + ("%%=", 0, "__divmod_u16u8u16u8"), + ("%%", 0, "__divmod_u16u8u16u8"), + ("%%=", 2, "__mod_u8u8u8u8"), + ("%%", 2, "__mod_u8u8u8u8"), + ("/=", 2, "__mod_u8u8u8u8"), + ("/", 2, "__mod_u8u8u8u8"), + ("/=", 2, "__div_u8u8u8u8"), + ("/", 2, "__div_u8u8u8u8"), ("+'", 4, "__adc_decimal"), ("+'=", 4, "__adc_decimal"), ("-'", 4, "__sub_decimal"), diff --git a/src/main/scala/millfork/output/AbstractAssembler.scala b/src/main/scala/millfork/output/AbstractAssembler.scala index 7b852370..4164505c 100644 --- a/src/main/scala/millfork/output/AbstractAssembler.scala +++ b/src/main/scala/millfork/output/AbstractAssembler.scala @@ -245,7 +245,7 @@ abstract class AbstractAssembler[T <: AbstractCode](private val program: Program } } - val unusedRuntimeObjects = Set("__mul_u8u8u8", "__constant8", "identity$", "__mul_u16u8u16").filterNot(name =>{ + val unusedRuntimeObjects = Set("__mul_u8u8u8", "__constant8", "identity$", "__mul_u16u8u16", "__divmod_u16u8u16u8", "__mod_u8u8u8u8", "__div_u8u8u8u8").filterNot(name =>{ compiledFunctions.exists{ case (fname, compiled) => fname != name && (compiled match { case f:NormalCompiledFunction[_] => f.code.exists(_.refersTo(name)) diff --git a/src/test/scala/millfork/test/ByteMathSuite.scala b/src/test/scala/millfork/test/ByteMathSuite.scala index 16ea160b..7d856c9d 100644 --- a/src/test/scala/millfork/test/ByteMathSuite.scala +++ b/src/test/scala/millfork/test/ByteMathSuite.scala @@ -314,8 +314,8 @@ class ByteMathSuite extends FunSuite with Matchers with AppendedClues { | void main () { | byte a | a = f() - | //output_q1 = a / $y - | //output_m1 = a %% $y + | output_q1 = a / $y + | output_m1 = a %% $y | output_q2 = a | output_m2 = a | output_q2 /= $y @@ -324,8 +324,53 @@ class ByteMathSuite extends FunSuite with Matchers with AppendedClues { | byte f() {return $x} """. stripMargin) { m => -// m.readByte(0xc000) should equal(x / y) withClue s"$x / $y" -// m.readByte(0xc001) should equal(x % y) withClue s"$x %% $y" + m.readByte(0xc000) should equal(x / y) withClue s"$x / $y" + m.readByte(0xc001) should equal(x % y) withClue s"$x %% $y" + m.readByte(0xc002) should equal(x / y) withClue s"$x / $y" + m.readByte(0xc003) should equal(x % y) withClue s"$x %% $y" + } + } + + test("Byte division 2") { + divisionCase2(0, 1) + divisionCase2(1, 1) + divisionCase2(2, 1) + divisionCase2(250, 1) + divisionCase2(0, 3) + divisionCase2(0, 5) + divisionCase2(1, 5) + divisionCase2(6, 5) + divisionCase2(73, 5) + divisionCase2(75, 5) + divisionCase2(42, 11) + } + + private def divisionCase2(x: Int, y: Int): Unit = { + EmuCrossPlatformBenchmarkRun(Cpu.Mos, Cpu.Z80, Cpu.Intel8080, Cpu.Sharp, Cpu.Intel8086)( + s""" + | import zp_reg + | byte output_q1 @$$c000 + | byte output_m1 @$$c001 + | byte output_q2 @$$c002 + | byte output_m2 @$$c003 + | void main () { + | byte a + | byte b + | a = f() + | b = g() + | output_q1 = a / b + | output_m1 = a %% b + | output_q2 = a + | output_m2 = a + | output_q2 /= b + | output_m2 %%= b + | } + | byte f() = $x + | noinline byte g() = $y + """. + stripMargin) { m => + m.readByte(0xc000) should equal(x / y) withClue s"$x / $y" + m.readByte(0xc001) should equal(x % y) withClue s"$x %% $y" m.readByte(0xc002) should equal(x / y) withClue s"$x / $y" m.readByte(0xc003) should equal(x % y) withClue s"$x %% $y" }