mirror of
https://github.com/KarolS/millfork.git
synced 2025-04-11 23:37:15 +00:00
Enable pointers to functions with a word parameter (using trampolines on 6502)
This commit is contained in:
parent
613ddcf9a4
commit
22b4776139
docs/lang
millfork-udl.xmlsrc
main/scala/millfork
assembly/mos/opt
compiler/mos
env
node
output
test/scala/millfork/test
@ -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`.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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">"sta " "lda " "jmp " "bit " "eor " "adc " "sbc " "ora " "and " "ldx " "ldy " "stx " "sty " "tax" "tay" "tya" "txa" "txs" "tsx" "sei" "cli" "clv" "clc" "cld" "sed" "sec" "bra " "beq " "bne " "bmi " "bpl " "bcc " "bcs " "bvs " bvc " "jsr " rts" "rti" "brk" "rol" "ror" "asl" "lsr" "inc " "dec " "cmp " "cpx " "cpy " inx iny dex dey pla pha plp hp phx plx phy ply "stz " "ldz " tza taz "tsb " "trb " ra txy tyx pld plb phb phd phk xce

"STA " "LDA " "JMP " "BIT " "EOR " "ADC " "SBC " "ORA " "AND " "LDX " "LDY " "STX " "STY " "TAX" "TAY" "TYA" "TXA" "TXS" "TSX" "SEI" "CLI" "CLV" "CLC" "CLD" "SED" "SEC" "BEQ " "BRA " "BNE " "BMI " "BPL " "BCC " "BCS " "BVS " BVC " "JSR " RTS" "RTI" "BRK" "ROL" "ROR" "ASL" "LSR" "INC " "DEC " "CMP " "CPX " "CPY " INX INY DEX DEY PLA PHA PLP HP PHX PLX PHY PLY "STZ " "LDZ " TZA TAZ "TSB " "TRB " RA TXY TYX PLD PLB PHB PHD PHK XCE</Keywords>
|
||||
<Keywords name="Keywords5">"sbx " "isc " "dcp " "lax " "sax " "anc " "alr " "arr " "rra " "rla " "lxa " "ane " "xaa "
"SBX " "ISC " "DCP " "LAX " "SAX " "ANC " "ALR " "ARR " "RRA " "RLA " "LXA " "ANE " "XAA "</Keywords>
|
||||
<Keywords name="Keywords6"></Keywords>
|
||||
|
@ -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 _ => ""
|
||||
|
@ -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)
|
||||
|
62
src/main/scala/millfork/env/Environment.scala
vendored
62
src/main/scala/millfork/env/Environment.scala
vendored
@ -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")
|
||||
|
29
src/main/scala/millfork/env/Thing.scala
vendored
29
src/main/scala/millfork/env/Thing.scala
vendored
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user