diff --git a/il65/src/il65/compiler/Compiler.kt b/il65/src/il65/compiler/Compiler.kt index 06a04c524..52b71b6c9 100644 --- a/il65/src/il65/compiler/Compiler.kt +++ b/il65/src/il65/compiler/Compiler.kt @@ -2,9 +2,115 @@ package il65.compiler import il65.ast.INameScope import il65.ast.Module +import java.nio.ByteBuffer +import java.nio.ByteOrder import kotlin.system.exitProcess +class CompilerException(message: String?) : Exception(message) + +// 5-byte cbm MFLPT format limitations: +const val FLOAT_MAX_POSITIVE = 1.7014118345e+38 +const val FLOAT_MAX_NEGATIVE = -1.7014118345e+38 + + +fun Number.toHex(): String { + // 0..15 -> "0".."15" + // 16..255 -> "$10".."$ff" + // 256..65536 -> "$0100".."$ffff" + val integer = this.toInt() + return when (integer) { + in 0 until 16 -> integer.toString() + in 0 until 0x100 -> "$"+integer.toString(16).padStart(2,'0') + in 0 until 0x10000 -> "$"+integer.toString(16).padStart(4,'0') + else -> throw CompilerException("number too large for 16 bits $this") + } +} + + +fun Double.frexp(): Pair { + var exponent: Int = Math.getExponent(this) + val mantissa: Double + + when (exponent) { + 1024 + -> { + // Inf or NaN + mantissa = this + exponent = 0 + } + + -1023 -> if (this == 0.0) { + // -0.0 or 0.0 + mantissa = this + exponent = 0 + } else { + exponent = Math.getExponent(this * 0x10000000000000) - 51 // that is 0x1p52 == 2**52 + mantissa = Math.scalb(this, -exponent) + } + + else -> { + exponent++ + mantissa = Math.scalb(this, -exponent) + } + } + return Pair(mantissa, exponent) +} + +fun Number.toMflpt5(): ShortArray { + // algorithm here https://sourceforge.net/p/acme-crossass/code-0/62/tree/trunk/ACME_Lib/cbm/mflpt.a + + // @todo fix this + + var flt = this.toDouble() + if(flt < FLOAT_MAX_NEGATIVE || flt > FLOAT_MAX_POSITIVE) + throw CompilerException("floating point number out of 5-byte mflpt range: $this") + if(flt==0.0) + return shortArrayOf(0, 0, 0, 0, 0) + val sign: Long = + when { + flt < 0.0 -> { + flt = -flt + 0x80000000L + } + else -> 0x00000000L + } + + var (mant, exp) = flt.frexp() + exp += 128 + if(exp < 1) { + // underflow, use zero instead + return shortArrayOf(0, 0, 0 ,0 ,0) + } + if(exp > 255) { + throw CompilerException("floating point number out of 5-byte mflpt range: $this") + } + val mflpt = sign.or((mant * 0x100000000L).toLong()).and(0x7fffffffL) + + + val result = ShortArray(5) + result[0] = exp.toShort() + result[1] = (mflpt and -0x1000000 shr 24).toShort() + result[2] = (mflpt and 0x00FF0000 shr 16).toShort() + result[3] = (mflpt and 0x0000FF00 shr 8).toShort() + result[4] = (mflpt and 0x000000FF shr 0).toShort() + return result + /* + + mant, exp = math.frexp(number) + exp += 128 + if exp < 1: + # underflow, use zero instead + return bytearray([0, 0, 0, 0, 0]) + if exp > 255: + raise OverflowError("floating point number out of 5-byte mflpt range", number) + mant = sign | int(mant * 0x100000000) & 0x7fffffff + return bytearray([exp]) + int.to_bytes(mant, 4, "big") + + */ +} + + class Compiler(val options: CompilationOptions, val namespace: INameScope) { init { val zeropage = Zeropage(options) diff --git a/il65/src/il65/compiler/Zeropage.kt b/il65/src/il65/compiler/Zeropage.kt index 629a02ee9..ebf6194d5 100644 --- a/il65/src/il65/compiler/Zeropage.kt +++ b/il65/src/il65/compiler/Zeropage.kt @@ -50,13 +50,13 @@ class Zeropage(private val options: CompilationOptions) { DataType.BYTE -> (vardecl.arrayspec.x as LiteralValue).intvalue!! DataType.WORD -> (vardecl.arrayspec.x as LiteralValue).intvalue!! * 2 DataType.FLOAT -> (vardecl.arrayspec.x as LiteralValue).intvalue!! * 5 - else -> throw UnsupportedOperationException("array can only be of byte, word, float") + else -> throw CompilerException("array can only be of byte, word, float") } } else { // 2 dimensional matrix (only bytes for now) when(vardecl.datatype) { DataType.BYTE -> (vardecl.arrayspec.x as LiteralValue).intvalue!! * y - else -> throw UnsupportedOperationException("matrix can only be of byte") + else -> throw CompilerException("matrix can only be of byte") } } } else { @@ -67,9 +67,9 @@ class Zeropage(private val options: CompilationOptions) { if (options.floats) { println("${vardecl.position} warning: allocating a large value in zeropage") 5 - } else throw UnsupportedOperationException("floating point option not enabled") + } else throw CompilerException("floating point option not enabled") } - else -> throw UnsupportedOperationException("cannot put datatype ${vardecl.datatype} in zeropage") + else -> throw CompilerException("cannot put datatype ${vardecl.datatype} in zeropage") } } @@ -87,7 +87,7 @@ class Zeropage(private val options: CompilationOptions) { } } - throw UnsupportedOperationException("ERROR: no free space in ZP to allocate $size sequential bytes") + throw CompilerException("ERROR: no free space in ZP to allocate $size sequential bytes") } private fun makeAllocation(location: Int, size: Int, datatype: DataType, name: String?): Int { diff --git a/il65/test/UnitTests.kt b/il65/test/UnitTests.kt index 85d3bcb30..79d5b68b1 100644 --- a/il65/test/UnitTests.kt +++ b/il65/test/UnitTests.kt @@ -4,10 +4,77 @@ import il65.ast.DataType import il65.ast.VarDecl import il65.ast.VarDeclType import il65.compiler.* +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import kotlin.test.assertEquals import kotlin.test.assertFailsWith + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TestCompiler { + @Test + fun testToHex() { + assertEquals("0", 0.toHex()) + assertEquals("1", 1.toHex()) + assertEquals("1", 1.234.toHex()) + assertEquals("10", 10.toHex()) + assertEquals("10", 10.99.toHex()) + assertEquals("15", 15.toHex()) + assertEquals("$10", 16.toHex()) + assertEquals("\$ff", 255.toHex()) + assertEquals("$0100", 256.toHex()) + assertEquals("$4e5c", 20060.toHex()) + assertEquals("\$ffff", 65535.toHex()) + assertEquals("\$ffff", 65535L.toHex()) + assertFailsWith { 65536.toHex() } + assertFailsWith { (-1).toHex() } + assertFailsWith { (-1.99).toHex() } + } + + + @Test + fun testFloatToMflpt5() { + assertThat((0).toMflpt5(), equalTo(shortArrayOf(0x00, 0x00, 0x00, 0x00, 0x00))) + assertThat((3.141592653).toMflpt5(), equalTo(shortArrayOf(0x82, 0x49, 0x0F, 0xDA, 0xA1))) + assertThat((3.141592653589793).toMflpt5(), equalTo(shortArrayOf(0x82, 0x49, 0x0F, 0xDA, 0xA2))) + assertThat((-32768).toMflpt5(), equalTo(shortArrayOf(0x90, 0x80, 0x00, 0x00, 0x00))) + assertThat((1).toMflpt5(), equalTo(shortArrayOf(0x81, 0x00, 0x00, 0x00, 0x00))) + assertThat((0.7071067812).toMflpt5(), equalTo(shortArrayOf(0x80, 0x35, 0x04, 0xF3, 0x34))) + assertThat((0.7071067811865476).toMflpt5(), equalTo(shortArrayOf(0x80, 0x35, 0x04, 0xF3, 0x33))) + assertThat((1.4142135624).toMflpt5(), equalTo(shortArrayOf(0x81, 0x35, 0x04, 0xF3, 0x34))) + assertThat((1.4142135623730951).toMflpt5(), equalTo(shortArrayOf(0x81, 0x35, 0x04, 0xF3, 0x33))) + assertThat((-.5).toMflpt5(), equalTo(shortArrayOf(0x80, 0x80, 0x00, 0x00, 0x00))) + assertThat((0.69314718061).toMflpt5(), equalTo(shortArrayOf(0x80, 0x31, 0x72, 0x17, 0xF8))) + assertThat((0.6931471805599453).toMflpt5(), equalTo(shortArrayOf(0x80, 0x31, 0x72, 0x17, 0xF7))) + assertThat((10).toMflpt5(), equalTo(shortArrayOf(0x84, 0x20, 0x00, 0x00, 0x00))) + assertThat((1000000000).toMflpt5(), equalTo(shortArrayOf(0x9E, 0x6E, 0x6B, 0x28, 0x00))) + assertThat((.5).toMflpt5(), equalTo(shortArrayOf(0x80, 0x00, 0x00, 0x00, 0x00))) + assertThat((1.4426950408889634).toMflpt5(), equalTo(shortArrayOf(0x81, 0x38, 0xAA, 0x3B, 0x29))) + assertThat((1.5707963267948966).toMflpt5(), equalTo(shortArrayOf(0x81, 0x49, 0x0F, 0xDA, 0xA2))) + assertThat((6.283185307179586).toMflpt5(), equalTo(shortArrayOf(0x83, 0x49, 0x0F, 0xDA, 0xA2))) + assertThat((.25).toMflpt5(), equalTo(shortArrayOf(0x7F, 0x00, 0x00, 0x00, 0x00))) + } + + @Test + fun testFloatRange() { + assertThat(FLOAT_MAX_POSITIVE.toMflpt5(), equalTo(shortArrayOf(0xff, 0x7f, 0xff, 0xff, 0xff))) + assertThat(FLOAT_MAX_NEGATIVE.toMflpt5(), equalTo(shortArrayOf(0xff, 0xff, 0xff, 0xff, 0xff))) + assertThat((1.7e-38).toMflpt5(), equalTo(shortArrayOf(0x03, 0x39, 0x1d, 0x15, 0x63))) + assertThat((1.7e-39).toMflpt5(), equalTo(shortArrayOf(0x00, 0x00, 0x00, 0x00, 0x00))) + assertThat((-1.7e-38).toMflpt5(), equalTo(shortArrayOf(0x03, 0xb9, 0x1d, 0x15, 0x63))) + assertThat((-1.7e-39).toMflpt5(), equalTo(shortArrayOf(0x00, 0x00, 0x00, 0x00, 0x00))) + assertFailsWith { 1.7014118346e+38.toMflpt5() } + assertFailsWith { (-1.7014118346e+38).toMflpt5() } + assertFailsWith { 1.7014118347e+38.toMflpt5() } + assertFailsWith { (-1.7014118347e+38).toMflpt5() } + } + + // @todo test the other way round, mflpt-bytes -> float. +} + + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TestZeropage { @Test @@ -30,7 +97,7 @@ class TestZeropage { @Test fun testZpFloatEnable() { val zp = Zeropage(CompilationOptions(OutputType.RAW, LauncherType.NONE, ZeropageType.FULL, false)) - assertFailsWith { + assertFailsWith { zp.allocate(VarDecl(VarDeclType.VAR, DataType.FLOAT, null, "", null)) } val zp2 = Zeropage(CompilationOptions(OutputType.RAW, LauncherType.NONE, ZeropageType.FULL, true)) @@ -41,7 +108,7 @@ class TestZeropage { fun testCompatibleAllocation() { val zp = Zeropage(CompilationOptions(OutputType.RAW, LauncherType.NONE, ZeropageType.COMPATIBLE, true)) assert(zp.available() == 9) - assertFailsWith { + assertFailsWith { // in regular zp there aren't 5 sequential bytes free zp.allocate(VarDecl(VarDeclType.VAR, DataType.FLOAT, null, "", null)) } @@ -50,10 +117,10 @@ class TestZeropage { assert(loc > 0) } assert(zp.available() == 0) - assertFailsWith { + assertFailsWith { zp.allocate(VarDecl(VarDeclType.VAR, DataType.BYTE, null, "", null)) } - assertFailsWith { + assertFailsWith { zp.allocate(VarDecl(VarDeclType.VAR, DataType.WORD, null, "", null)) } } @@ -73,7 +140,7 @@ class TestZeropage { } assert(zp.available() == 19) - assertFailsWith { + assertFailsWith { // can't allocate because no more sequential bytes, only fragmented zp.allocate(VarDecl(VarDeclType.VAR, DataType.FLOAT, null, "", null)) } @@ -86,7 +153,7 @@ class TestZeropage { assert(zp.available() == 1) zp.allocate(VarDecl(VarDeclType.VAR, DataType.BYTE, null, "", null)) - assertFailsWith { + assertFailsWith { // no more space zp.allocate(VarDecl(VarDeclType.VAR, DataType.BYTE, null, "", null)) }