Skip to content

Commit

Permalink
Adds support for Ion Schema 2.0 ieee754_float constraint
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt committed Oct 6, 2022
1 parent d074c29 commit 9b6e58b
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.amazon.ionschema.internal.constraint.Contains
import com.amazon.ionschema.internal.constraint.Content
import com.amazon.ionschema.internal.constraint.Element
import com.amazon.ionschema.internal.constraint.Fields
import com.amazon.ionschema.internal.constraint.Ieee754Float
import com.amazon.ionschema.internal.constraint.Not
import com.amazon.ionschema.internal.constraint.OccursNoop
import com.amazon.ionschema.internal.constraint.OneOf
Expand Down Expand Up @@ -72,6 +73,7 @@ internal class ConstraintFactoryDefault : ConstraintFactory {
ConstraintConstructor("content", v1_0, ::Content),
ConstraintConstructor("element", v1_0, ::Element),
ConstraintConstructor("fields", v1_0, ::Fields),
ConstraintConstructor("ieee754_float", v2_0, ::Ieee754Float),
ConstraintConstructor("not", v1_0..v2_0, ::Not),
ConstraintConstructor("occurs", v1_0, ::OccursNoop),
ConstraintConstructor("one_of", v1_0..v2_0, ::OneOf),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

package com.amazon.ionschema.internal.constraint

import com.amazon.ion.IonFloat
import com.amazon.ion.IonSymbol
import com.amazon.ion.IonValue
import com.amazon.ionschema.Violation
import com.amazon.ionschema.Violations
import com.amazon.ionschema.internal.constraint.Ieee754Float.Ieee754InterchangeFormat.binary16
import com.amazon.ionschema.internal.constraint.Ieee754Float.Ieee754InterchangeFormat.binary32
import com.amazon.ionschema.internal.constraint.Ieee754Float.Ieee754InterchangeFormat.binary64
import com.amazon.ionschema.internal.util.islRequire
import com.amazon.ionschema.internal.util.islRequireIonTypeNotNull
import com.amazon.ionschema.internal.util.islRequireNotNull
import kotlin.math.absoluteValue

/**
* Implements the ieee754_float constraint.
*
* @see https://amzn.github.io/ion-schema/docs/isl-2-0/spec#ieee754_float
*/
internal class Ieee754Float(ion: IonValue) : ConstraintBase(ion) {

/**
* Represents a range of floating point numbers and the interval between the representable values in that range.
*/
private data class PrecisionInfo(val range: ClosedRange<Double>, val interval: Double)

companion object {
/**
* Available precision can be calculated to produce a table, such as the following table for 16-bit floats.
*
* Rather than create a generalized solution, we're going to hardcode some values. This is justifiable because
* 16-bit floats are small enough that we can do it, this is the only floating point format for which it is
* needed, and it avoids the complexity of a generalized solution.
*
* Technically, there is overlap between the ranges, but that's okay because the overlap is only the boundary
* value, which will be valid regardless of which interval we test it against.
*/
private val BINARY_16_PRECISION_RANGES = listOf(
PrecisionInfo(0.0..1.0 / 16_384, interval = 1.0 / 16_777_216), // Subnormal numbers
PrecisionInfo(1.0 / 16_384..1.0 / 8_192, interval = 1.0 / 16_777_216),
PrecisionInfo(1.0 / 8_192..1.0 / 4_096, interval = 1.0 / 8_388_608),
PrecisionInfo(1.0 / 4_096..1.0 / 2_048, interval = 1.0 / 4_194_304),
PrecisionInfo(1.0 / 2_048..1.0 / 1_024, interval = 1.0 / 2_097_152),
PrecisionInfo(1.0 / 1_024..1.0 / 512, interval = 1.0 / 1_048_576),
PrecisionInfo(1.0 / 512..1.0 / 256, interval = 1.0 / 524_288),
PrecisionInfo(1.0 / 256..1.0 / 128, interval = 1.0 / 262_144),
PrecisionInfo(1.0 / 128..1.0 / 64, interval = 1.0 / 131_072),
PrecisionInfo(1.0 / 64..1.0 / 32, interval = 1.0 / 65_536),
PrecisionInfo(1.0 / 32..1.0 / 16, interval = 1.0 / 32_768),
PrecisionInfo(1.0 / 16..1.0 / 8, interval = 1.0 / 16_384),
PrecisionInfo(1.0 / 8..1.0 / 4, interval = 1.0 / 8_192),
PrecisionInfo(1.0 / 4..1.0 / 2, interval = 1.0 / 4_096),
PrecisionInfo(1.0 / 2..1.0, interval = 1.0 / 2_048),
PrecisionInfo(1.0..2.0, interval = 1.0 / 1_024),
PrecisionInfo(2.0..4.0, interval = 1.0 / 512),
PrecisionInfo(4.0..8.0, interval = 1.0 / 256),
PrecisionInfo(8.0..16.0, interval = 1.0 / 128),
PrecisionInfo(16.0..32.0, interval = 1.0 / 64),
PrecisionInfo(32.0..64.0, interval = 1.0 / 32),
PrecisionInfo(64.0..128.0, interval = 1.0 / 16),
PrecisionInfo(128.0..256.0, interval = 1.0 / 8),
PrecisionInfo(256.0..512.0, interval = 1.0 / 4),
PrecisionInfo(512.0..1_024.0, interval = 1.0 / 2),
PrecisionInfo(1_024.0..2_048.0, interval = 1.0),
PrecisionInfo(2_048.0..4_096.0, interval = 2.0),
PrecisionInfo(4_096.0..8_192.0, interval = 4.0),
PrecisionInfo(8_192.0..16_384.0, interval = 8.0),
PrecisionInfo(16_384.0..32_768.0, interval = 16.0),
PrecisionInfo(32_768.0..65_504.0, interval = 32.0),
)
}

/**
* The possible values for the `ieee754_float` constraint.
*/
private enum class Ieee754InterchangeFormat {
binary16,
binary32,
binary64;
companion object {
val names = Ieee754InterchangeFormat.values().map { it.name }
}
}

private val interchangeFormat = ion.let {
islRequireIonTypeNotNull<IonSymbol>(it) { "ieee754_float must be a non-null Ion symbol" }
islRequire(it.typeAnnotations.isEmpty()) { "ieee754_float must not have annotations" }
islRequire(it.stringValue() in Ieee754InterchangeFormat.names) { "ieee754_float must be one of ${Ieee754InterchangeFormat.names}" }
return@let Ieee754InterchangeFormat.valueOf(it.stringValue())
}

override fun validate(value: IonValue, issues: Violations) {
validateAs<IonFloat>(value, issues) {
if (!(interchangeFormat losslesslyEncodes it)) {
issues.add(Violation(ion, "invalid_ieee754_float", "value cannot be losslessly represented by the IEEE-754 $interchangeFormat interchange format."))
}
}
}

private infix fun Ieee754InterchangeFormat.losslesslyEncodes(value: IonFloat): Boolean {
// +inf, -inf, and nan are always valid
if (!value.doubleValue().isFinite()) return true

return when (this) {
binary16 -> value.doubleValue().isExactHalfValue()
binary32 -> value.doubleValue().toFloat().toDouble() == value.doubleValue()
binary64 -> true // All Ion Floats are 64 bits or smaller.
}
}

private fun Double.isExactHalfValue(): Boolean {
if (this.absoluteValue > 65504) return false
// Find the appropriate interval for the Double value we are testing
val (_, interval) = BINARY_16_PRECISION_RANGES.first { (range, _) -> absoluteValue in range }
// If the value is an exact multiple of the interval between two Half values, then we know it
// is possible to _exactly_ represent the value as a Half precision float.
return this % interval == 0.0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class IonSchemaTests_2_0 : TestFactory by IonSchemaTestsRunner(
it.path.endsWith("constraints/codepoint_length.isl") ||
it.path.endsWith("constraints/container_length.isl") ||
it.path.endsWith("constraints/contains.isl") ||
it.path.endsWith("constraints/ieee754_float.isl") ||
it.path.endsWith("constraints/not.isl") ||
// TODO: Add "one_of" tests once annotations support is added
it.path.endsWith("constraints/precision.isl") ||
Expand Down

0 comments on commit 9b6e58b

Please sign in to comment.