Add Number.noise to the standard library (#1295)

This commit is contained in:
Ara Adkins 2020-11-18 13:03:28 +00:00 committed by GitHub
parent 4d5f794122
commit cf9be4ff29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 478 additions and 12 deletions

View File

@ -0,0 +1,71 @@
from Base import all
import Base.Data.Interval.Bound
export Base.Data.Interval.Bound
## Creates an interval that excludes both its bounds.
> Example
Create the bounds-exclusive range from 1 to 5.
Interval.exclusive 1 5
exclusive : Any -> Any -> Interval
exclusive start end = Interval (Bound.Exclusive start) (Bound.Exclusive end)
## Creates an interval that excludes its lower bound.
> Example
Create the start-exclusive range from 1 to 5.
Interval.start_exclusive 1 5
start_exclusive : Any -> Any -> Interval
start_exclusive start end = Interval (Bound.Exclusive start) (Bound.Inclusive end)
## Creates an interval that excludes its upper bound.
> Example
Create the end-exclusive range from 1 to 5.
Interval.end_exclusive 1 5
end_exclusive : Any -> Any -> Interval
end_exclusive start end = Interval (Bound.Inclusive start) (Bound.Exclusive end)
## Creates an interval that includes its upper bound.
> Example
Create the inclusive range from 1 to 5.
Interval.inclusive 1 5
inclusive : Any -> Any -> Interval
inclusive start end = Interval (Bound.Inclusive start) (Bound.Inclusive end)
## An interval type
type Interval
type Interval start end
## Checks if the interval contains `that`.
> Example
Checking if the interval 1 to 5 contains 7.
(Interval.inclusive 1 5) . contains 7
contains : Any -> Boolean
contains that = if this.start.n > this.end.n then False else
case this.start of
Bound.Exclusive s -> (that > s) && case this.end of
Bound.Exclusive e -> that < e
Bound.Inclusive e -> that <= e
Bound.Inclusive s -> (that >= s) && case this.end of
Bound.Exclusive e -> that < e
Bound.Inclusive e -> that <= e
## Check if this interval is empty.
is_empty : Boolean
is_empty = case this.start of
Bound.Exclusive s -> case this.end of
Bound.Exclusive e -> s >= e
Bound.Inclusive e -> s >= e
Bound.Inclusive s -> case this.end of
Bound.Exclusive e -> s >= e
Bound.Inclusive e -> s > e
## Check if this interval is not empty.
not_empty : Boolean
not_empty = this.is_empty.not

View File

@ -0,0 +1,11 @@
from Base import all
## A type representing an interval bound over any orderable type.
An orderable type is one that
type Bound
## A bound that includes `n`.
type Inclusive n
## A bound that excludes `n`.
type Exclusive n

View File

@ -0,0 +1,19 @@
from Base import all
from Base.Data.Noise.Generator import all
## Generate noise based on the input number.
The output of the noise generator will depend on the input and the range over
which the noise is being generated.
By default, this uses a seeded deterministic generator that will always
return the same input for the same output. In addition, it will, by default,
generate values in the exclusive range 0 to 1.
> Example
Deterministically perturb the input number 1.
1.noise
Number.noise : Interval -> Generator -> Any
Number.noise (interval = Interval.exclusive 0 1) gen=Deterministic_Random =
gen.step this interval

View File

@ -0,0 +1,45 @@
from Base import all
import Base.Data.Interval.Bound
import Base.Error.Extensions
polyglot java import java.util.Random
polyglot java import java.lang.Long
## The interface for the noise generator abstraction.
To be a valid generator, it must provide the `step` method as described
below.
type Generator
type Generator
## Step the generator to produce the next value..
The parameters are as follows:
- The input number, which is intended for use as a seed.
- A range for output values, which should range over the chosen output
type.
The return type may be chosen freely by the generator implementation, as
it usually depends on the generator and its intended use.
step : Number -> Interval -> Any
step _ _ = Unimplemented "Only intended to demonstrate an interface."
## A noise generator that implements a seeded deterministic random peterbation
of the input.
It produces what is commonly termed "white" noise, where any value in the
range has an equal chance of occurring.
type Deterministic_Random
type Deterministic_Random
## Step the generator to produce the next value.
step : Number -> Interval -> Number
step input interval =
max_long = Polyglot.get_member Long "MAX_VALUE"
seed = input.floor % max_long
gen = Random.new [seed].to_array
value_range = (interval.end.n - interval.start.n).abs
offset = (interval.start.n)
val = gen.nextDouble []
(val * value_range) + offset

View File

@ -94,3 +94,4 @@ Number.max that = if this > that then this else that
## Number to JSON conversion.
Number.to_json : Json.Number
Number.to_json = Json.Number this

View File

@ -4,3 +4,11 @@ from Base import all
No_Such_Method_Error.method_name : Text
No_Such_Method_Error.method_name =
Meta.meta this.symbol . name
## A type used to represent that something has not yet been implemented.
type Unimplemented_Error message
## A function that can be used to indicate that something hasn't been
implemented yet.
unimplemented : Text -> Void ! Unimplemented_Error
unimplemented message="" = Panic.throw (Unimplemented_Error message)

View File

@ -1,7 +1,9 @@
import Base.Data.Any.Extensions
import Base.Data.Interval
import Base.Data.Json
import Base.Data.List
import Base.Data.Map
import Base.Data.Noise
import Base.Data.Number.Extensions
import Base.Data.Pair
import Base.Data.Range
@ -16,6 +18,7 @@ import Base.System.File
from Builtins import Nothing, Number, Integer, Any, True, False, Cons, Boolean
export Base.Data.Interval
export Base.Data.Json
export Base.Data.Map
export Base.Data.Vector
@ -25,6 +28,7 @@ export Base.System.File
from Base.Data.Any.Extensions export all
from Base.Data.List export Nil, Cons
from Base.Data.Noise export all hiding Noise
from Base.Data.Number.Extensions export all hiding Math, String
from Base.Data.Pair export Pair
from Base.Data.Range export Range

View File

@ -48,6 +48,22 @@ Any.should verb argument = Verbs.verb this argument
## Fail a test with the given message.
fail message = Panic.throw (Failure message)
## Expect a function to fail with the provided error.
expect_fail_with ~action matcher =
res = Panic.recover action
case res of
_ -> here.fail ("Expected a " + matcher.to_text + " to be thrown, but the action succeeded.")
err = res.catch x->x
case Meta.meta matcher of
Meta.Atom _ -> if err == matcher then Success else
here.fail ("Expected a " + matcher.to_text + "be thrown, but found " + err.to_text + ".")
Meta.Constructor _ ->
meta_err = Meta.meta err
case meta_err of
Meta.Atom _ -> if meta_err.constructor == matcher then Success else
here.fail ("Unexpected error " + err.to_text + " thrown.")
_ -> here.fail ("Incorrect instance " + err.to_text + ".")
## Asserts that `this` value is equal to the expected value.
Any.should_equal that = case this == that of
True -> Success

View File

@ -0,0 +1,12 @@
package org.enso.interpreter.node.expression.builtin.number.bigInteger;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(type = "Big_Integer", name = "ceil", description = "Big integer ceiling.")
public class CeilNode extends Node {
Object execute(EnsoBigInteger _this) {
return _this;
}
}

View File

@ -0,0 +1,12 @@
package org.enso.interpreter.node.expression.builtin.number.bigInteger;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(type = "Big_Integer", name = "floor", description = "Big integer floor.")
public class FloorNode extends Node {
Object execute(EnsoBigInteger _this) {
return _this;
}
}

View File

@ -0,0 +1,25 @@
package org.enso.interpreter.node.expression.builtin.number.decimal;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.profiles.ConditionProfile;
import java.math.BigDecimal;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(
type = "Decimal",
name = "ceil",
description = "Decimal ceiling, converting to a small or big integer depending on size.")
public class CeilNode extends Node {
private final ConditionProfile fitsProfile = ConditionProfile.createCountingProfile();
Object execute(double _this) {
double ceil = Math.ceil(_this);
if (fitsProfile.profile(BigIntegerOps.fitsInLong(ceil))) {
return (long) ceil;
} else {
return new EnsoBigInteger(BigDecimal.valueOf(ceil).toBigIntegerExact());
}
}
}

View File

@ -0,0 +1,25 @@
package org.enso.interpreter.node.expression.builtin.number.decimal;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.profiles.ConditionProfile;
import java.math.BigDecimal;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.node.expression.builtin.number.utils.BigIntegerOps;
import org.enso.interpreter.runtime.number.EnsoBigInteger;
@BuiltinMethod(
type = "Decimal",
name = "floor",
description = "Decimal floor, converting to a small or big integer depending on size.")
public class FloorNode extends Node {
private final ConditionProfile fitsProfile = ConditionProfile.createCountingProfile();
Object execute(double _this) {
double floor = Math.floor(_this);
if (fitsProfile.profile(BigIntegerOps.fitsInLong(floor))) {
return (long) floor;
} else {
return new EnsoBigInteger(BigDecimal.valueOf(floor).toBigIntegerExact());
}
}
}

View File

@ -0,0 +1,11 @@
package org.enso.interpreter.node.expression.builtin.number.smallInteger;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
@BuiltinMethod(type = "Small_Integer", name = "ceil", description = "Small integer ceiling.")
public class CeilNode extends Node {
long execute(long _this) {
return _this;
}
}

View File

@ -0,0 +1,11 @@
package org.enso.interpreter.node.expression.builtin.number.smallInteger;
import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.dsl.BuiltinMethod;
@BuiltinMethod(type = "Small_Integer", name = "floor", description = "Small integer floor.")
public class FloorNode extends Node {
long execute(long _this) {
return _this;
}
}

View File

@ -6,6 +6,9 @@ import java.math.BigInteger;
/** Re-exposes big-integer operations behind a truffle boundary. */
public class BigIntegerOps {
private static final BigInteger MIN_LONG_BIGINT = BigInteger.valueOf(Long.MIN_VALUE);
private static final BigInteger MAX_LONG_BIGINT = BigInteger.valueOf(Long.MAX_VALUE);
@CompilerDirectives.TruffleBoundary
public static BigInteger multiply(long a, long b) {
return BigInteger.valueOf(a).multiply(BigInteger.valueOf(b));
@ -125,4 +128,13 @@ public class BigIntegerOps {
}
return res;
}
@CompilerDirectives.TruffleBoundary
public static boolean fitsInLong(BigInteger bigInteger) {
return bigInteger.compareTo(MIN_LONG_BIGINT) >= 0 && bigInteger.compareTo(MAX_LONG_BIGINT) <= 0;
}
public static boolean fitsInLong(double decimal) {
return decimal <= Long.MAX_VALUE && decimal >= Long.MIN_VALUE;
}
}

View File

@ -14,9 +14,6 @@ import java.math.BigInteger;
public class ToEnsoNumberNode extends Node {
private final ConditionProfile fitsProfile = ConditionProfile.createCountingProfile();
private static final BigInteger MIN_LONG_BIGINT = BigInteger.valueOf(Long.MIN_VALUE);
private static final BigInteger MAX_LONG_BIGINT = BigInteger.valueOf(Long.MAX_VALUE);
/** @return a new instance of this node. */
public static ToEnsoNumberNode build() {
return new ToEnsoNumberNode();
@ -30,17 +27,12 @@ public class ToEnsoNumberNode extends Node {
* org.enso.interpreter.runtime.number.EnsoBigInteger} otherwise.
*/
public Object execute(BigInteger bigInteger) {
if (fitsProfile.profile(fitsInLong(bigInteger))) {
if (fitsProfile.profile(BigIntegerOps.fitsInLong(bigInteger))) {
return toLong(bigInteger);
}
return new EnsoBigInteger(bigInteger);
}
@CompilerDirectives.TruffleBoundary
private static boolean fitsInLong(BigInteger bigInteger) {
return bigInteger.compareTo(MIN_LONG_BIGINT) >= 0 && bigInteger.compareTo(MAX_LONG_BIGINT) <= 0;
}
@CompilerDirectives.TruffleBoundary
private static long toLong(BigInteger bigInteger) {
return bigInteger.longValue();

View File

@ -112,6 +112,16 @@ public class Number {
"to_decimal",
org.enso.interpreter.node.expression.builtin.number.smallInteger.ToDecimalMethodGen
.makeFunction(language));
scope.registerMethod(
smallInteger,
"floor",
org.enso.interpreter.node.expression.builtin.number.smallInteger.FloorMethodGen
.makeFunction(language));
scope.registerMethod(
smallInteger,
"ceil",
org.enso.interpreter.node.expression.builtin.number.smallInteger.CeilMethodGen.makeFunction(
language));
}
private void registerBigIntegerMethods(Language language, ModuleScope scope) {
@ -191,6 +201,16 @@ public class Number {
"to_decimal",
org.enso.interpreter.node.expression.builtin.number.bigInteger.ToDecimalMethodGen
.makeFunction(language));
scope.registerMethod(
bigInteger,
"floor",
org.enso.interpreter.node.expression.builtin.number.bigInteger.FloorMethodGen.makeFunction(
language));
scope.registerMethod(
bigInteger,
"ceil",
org.enso.interpreter.node.expression.builtin.number.bigInteger.CeilMethodGen.makeFunction(
language));
}
private void registerDecimalMethods(Language language, ModuleScope scope) {
@ -260,6 +280,16 @@ public class Number {
"to_decimal",
org.enso.interpreter.node.expression.builtin.number.decimal.ToDecimalMethodGen.makeFunction(
language));
scope.registerMethod(
decimal,
"floor",
org.enso.interpreter.node.expression.builtin.number.decimal.FloorMethodGen.makeFunction(
language));
scope.registerMethod(
decimal,
"ceil",
org.enso.interpreter.node.expression.builtin.number.decimal.CeilMethodGen.makeFunction(
language));
}
/** @return the Int64 atom constructor. */

View File

@ -1,11 +1,15 @@
package org.enso.interpreter.runtime.number;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.TruffleObject;
import com.oracle.truffle.api.library.ExportLibrary;
import com.oracle.truffle.api.library.ExportMessage;
import java.math.BigInteger;
/** Internal wrapper for a {@link BigInteger}. */
@ExportLibrary(InteropLibrary.class)
public class EnsoBigInteger implements TruffleObject {
private final BigInteger value;
@ -28,4 +32,9 @@ public class EnsoBigInteger implements TruffleObject {
public String toString() {
return value.toString();
}
@ExportMessage
String toDisplayString(boolean allowSideEffects) {
return value.toString();
}
}

View File

@ -448,8 +448,8 @@ object Builtin {
sequenceLiteral,
typesetLiteral,
case_of,
if_then,
if_then_else,
if_then,
polyglotJavaImport,
itemsImport,
qualifiedImport,

View File

@ -0,0 +1,90 @@
from Base import all
import Test
spec =
describe "Bound" <|
it "should allow constructing inclusive bounds" <|
bound = Interval.Bound.Inclusive 0
bound.n . should_equal 0
it "should allow constructing exclusive bounds" <|
bound = Interval.Bound.Exclusive 0
bound.n . should_equal 0
it "should be able to be checked for equality" <|
inclusive_1 = Interval.Bound.Inclusive 10
inclusive_2 = Interval.Bound.Inclusive 5
exclusive_1 = Interval.Bound.Exclusive 10
exclusive_2 = Interval.Bound.Exclusive 5
(inclusive_1 == inclusive_1) . should_be_true
(inclusive_1 == inclusive_2) . should_be_false
(exclusive_1 == exclusive_1) . should_be_true
(exclusive_1 == exclusive_2) . should_be_false
(inclusive_1 == exclusive_1) . should_be_false
describe "Interval" <|
it "should allow constructing exclusive intervals" <|
interval = Interval.exclusive 1 5
interval.start . should_equal (Interval.Bound.Exclusive 1)
interval.end . should_equal (Interval.Bound.Exclusive 5)
it "should allow constructing start-exclusive intervals" <|
interval = Interval.start_exclusive 1 5
interval.start . should_equal (Interval.Bound.Exclusive 1)
interval.end . should_equal (Interval.Bound.Inclusive 5)
it "should allow constructing end-exclusive intervals" <|
interval = Interval.end_exclusive 1 5
interval.start . should_equal (Interval.Bound.Inclusive 1)
interval.end . should_equal (Interval.Bound.Exclusive 5)
it "should allow constructing inclusive intervals" <|
interval = Interval.inclusive 1 5
interval.start . should_equal (Interval.Bound.Inclusive 1)
interval.end . should_equal (Interval.Bound.Inclusive 5)
it "should allow checking if an interval contains a value of the contained type" <|
interval = Interval.end_exclusive 1 10
interval.contains 0 . should_be_false
interval.contains 1 . should_be_true
interval.contains 9 . should_be_true
interval.contains 10 . should_be_false
interval.contains 10 . should_be_false
interval_2 = Interval.end_exclusive 0 0
interval_2.contains -1 . should_be_false
interval_2.contains 0 . should_be_false
interval_2.contains 1 . should_be_false
interval_3 = Interval.end_exclusive 0 1
interval_3.contains -1 . should_be_false
interval_3.contains 0 . should_be_true
interval_3.contains 1 . should_be_false
interval_4 = Interval.inclusive 0 0
interval_4.contains -1 . should_be_false
interval_4.contains 0 . should_be_true
interval_4.contains 1 . should_be_false
interval_5 = Interval.exclusive 0 0
interval_5.contains -1 . should_be_false
interval_5.contains 0 . should_be_false
interval_5.contains 1 . should_be_false
interval_6 = Interval.start_exclusive 0 0
interval_6.contains -1 . should_be_false
interval_6.contains 0 . should_be_false
interval_6.contains 1 . should_be_false
it "can be checked for emptiness" <|
Interval.exclusive 0 0 . is_empty . should_be_true
Interval.exclusive 1 10 . is_empty . should_be_false
Interval.start_exclusive 0 0 . is_empty . should_be_true
Interval.start_exclusive 1 10 . is_empty . should_be_false
Interval.end_exclusive 0 0 . is_empty . should_be_true
Interval.end_exclusive 1 10 . is_empty . should_be_false
Interval.inclusive 0 0 . is_empty . should_be_false
Interval.inclusive 10 0 . is_empty . should_be_true
it "can be checked for non-emptiness" <|
Interval.exclusive 0 0 . not_empty . should_be_false
Interval.exclusive 1 10 . not_empty . should_be_true
Interval.start_exclusive 0 0 . not_empty . should_be_false
Interval.start_exclusive 1 10 . not_empty . should_be_true
Interval.end_exclusive 0 0 . not_empty . should_be_false
Interval.end_exclusive 1 10 . not_empty . should_be_true
Interval.inclusive 0 0 . not_empty . should_be_true
Interval.inclusive 10 0 . not_empty . should_be_false

View File

@ -0,0 +1,24 @@
from Base import all
import Base.Data.Noise.Generator
import Base.Error.Extensions
import Test
spec =
describe "Generator Interface" <|
gen = Generator.Generator
it "should not be invokable" <|
interval = Interval.inclusive 0 1
Test.expect_fail_with (gen.step 1 interval) Extensions.Unimplemented_Error
describe "Deterministic Random Noise Generator" <|
gen = Generator.Deterministic_Random
it "should always return the same output for the same input" <|
interval = Interval.inclusive 0 1
values = Vector.fill 10000 1 . map (gen.step _ interval)
values.all (== values.at 0) . should_be_true
it "should always produce values within the specified interval" <|
interval = Interval.inclusive -100 100
values = 1.up_to 10000 . to_vector . map (gen.step _ interval)
values.all (v -> (v >= -100) && (v <= 100)) . should_be_true

View File

@ -0,0 +1,19 @@
from Base import all
import Test
type My_Generator
My_Generator.step _ _ = 1
spec = describe "Noise" <|
it "should be able to be called on numbers" <|
result = 1.noise
result-result . should_equal 0
it "should allow the user to specify a generator" <|
result = 1.noise (gen=My_Generator)
result-result . should_equal 0
it "should allow the user to specify the interval" <|
interval = Interval.inclusive -250 250
values = 1.up_to 10000 . to_vector . map (_.noise interval)
values.all (v -> (v >= -250) && (v <= 250)) . should_be_true

View File

@ -9,14 +9,15 @@ spec =
eps = 0.000001
almost_max_long = 9223372036854775806
almost_max_long_times_three = 27670116110564327418
almost_max_long_times_three_plus_1 = 27670116110564327419
almost_max_long_times_three_decimal = 27670116110564327418.8
hundred_factorial = 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
describe "Integers" <|
it "should be of unbound size when multiplied" <|
1.up_to 101 . fold 1 (*) . should_equal hundred_factorial
it "should be of unbound size when added" <|
(almost_max_long + almost_max_long + almost_max_long).should_equal almost_max_long_times_three
it "should be of unbound size when subtracted" <|
(0 - almost_max_long - almost_max_long - almost_max_long).should_equal almost_max_long_times_three.negate
it "should be of unbound size when subtracted" <| (0 - almost_max_long - almost_max_long - almost_max_long).should_equal almost_max_long_times_three.negate
it "should be of unbound size when dividing" <|
expected = 3372816184472482867110284450043137767873196479305249187406461598235841786750685581361224832688174410089430537516012695688121622150430744676
((1.up_to 101 . fold 1 (*)).div 3*almost_max_long).should_equal expected
@ -70,3 +71,14 @@ spec =
(Math.Pi / 6).cos.should_equal (3.sqrt / 2) epsilon=eps
(17 ^ 0.13).log base=17 . should_equal 0.13 epsilon=eps
0.exp.should_equal 1
it "should allow calculating the floor value" <|
1.2314.floor . should_equal 1
1.floor . should_equal 1
almost_max_long_times_three_decimal.floor.to_decimal . should_equal almost_max_long_times_three.to_decimal
almost_max_long_times_three.floor . should_equal almost_max_long_times_three
it "should allow calculating the ceil value" <|
1.2314.ceil . should_equal 2
1.ceil . should_equal 1
almost_max_long_times_three_decimal.ceil.to_decimal . should_equal almost_max_long_times_three_plus_1.to_decimal
almost_max_long_times_three_plus_1.ceil . should_equal almost_max_long_times_three_plus_1

View File

@ -7,9 +7,12 @@ import Tests.Semantic.Java_Interop_Spec
import Tests.Semantic.Meta_Spec
import Tests.Semantic.Names_Spec
import Tests.Data.Interval_Spec
import Tests.Data.Json_Spec
import Tests.Data.List_Spec
import Tests.Data.Map_Spec
import Tests.Data.Noise.Generator_Spec
import Tests.Data.Noise_Spec
import Tests.Data.Numbers_Spec
import Tests.Data.Range_Spec
import Tests.Data.Text_Spec
@ -27,15 +30,18 @@ main = Test.Suite.runMain <|
Deep_Export_Spec.spec
Error_Spec.spec
File_Spec.spec
Generator_Spec.spec
Header_Spec.spec
Http_Spec.spec
Import_Loop_Spec.spec
Interval_Spec.spec
Java_Interop_Spec.spec
Json_Spec.spec
List_Spec.spec
Map_Spec.spec
Meta_Spec.spec
Names_Spec.spec
Noise_Spec.spec
Numbers_Spec.spec
Process_Spec.spec
Range_Spec.spec

View File

@ -22,3 +22,4 @@ spec = describe "Meta-Value Manipulation" <|
it "should correctly return representations of different classes of objects" <|
Meta.meta 1 . should equal (Meta.Primitive 1)
Meta.meta "foo" . should equal (Meta.Primitive "foo")