package scotch.compiler.target;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static scotch.data.either.Either.left;
import static scotch.data.list.ConsList.eagerCons;
import static scotch.data.maybe.Maybe.just;
import static scotch.data.ratio.Ratio.ratio;
import static scotch.data.tuple.TupleValues.tuple2;
import static scotch.data.tuple.TupleValues.tuple3;
import java.io.File;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.Optional;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import scotch.compiler.ClassLoaderResolver;
import scotch.compiler.error.CompileException;
import scotch.util.TestUtil;
import scotch.data.either.Either.Left;
import scotch.data.list.ConsList;
import scotch.data.maybe.Maybe;
import scotch.data.ratio.Ratio;
import scotch.data.tuple.Tuple2;
import scotch.data.tuple.Tuple3;
import scotch.runtime.Callable;
import scotch.runtime.RaisedException;
public class BytecodeGeneratorTest {
@Rule
public final TestName testName = new TestName();
@Test
public void shouldCompileId() {
String result = exec(
"module scotch.test",
"id = \\x -> x",
"run = id \"Bananas!\""
);
assertThat(result, is("Bananas!"));
}
@Test
public void shouldCompile2Plus2() {
int result = exec(
"module scotch.test",
"run = 2 + 2"
);
assertThat(result, is(4));
}
@Test
public void shouldCompileDelegated2Plus2() {
int result = exec(
"module scotch.test",
"add = \\x y -> x + y",
"run = add 2 2"
);
assertThat(result, is(4));
}
@Test
public void shouldCompile2Plus2WithDoubles() {
double result = exec(
"module scotch.test",
"add = \\x y -> x + y",
"run = add 2.2 2.2"
);
assertThat(result, is(4.4));
}
@Test(expected = CompileException.class)
public void shouldFailCompilation_whenThereAreErrors() {
exec(
"module scotch.test",
"add = \\x y -> x + y",
"run = add 2.2 2"
);
}
@Test
public void shouldPassNamedFunctionAsArgument() {
int result = exec(
"module scotch.test",
"import scotch.data.num", // TODO should not require import
"fn a b c d = d a b c",
"run = fn 1 2 3 add3",
"add3 x y z = x + y + z"
);
assertThat(result, is(6));
}
@Test
public void shouldPassAnonymousFunctionAsArgument() {
int result = exec(
"module scotch.test",
"import scotch.data.num", // TODO should not require import
"fn a b c d = d a b c",
"run = fn 1 2 3 (\\x y z -> x + y + z)"
);
assertThat(result, is(6));
}
@Test
public void shouldCompileConditional() {
String result = exec(
"module scotch.test",
"run = if True then \"Waffles\" else \"Bananas\""
);
assertThat(result, is("Waffles"));
}
@Test
public void shouldCompileChainedConditional() {
int result = exec(
"module scotch.test",
"",
"run = fib 20",
"fib = \\n -> if n == 0 then 0",
" else if n == 1 then 1",
" else fib (n - 1) + fib (n - 2)"
);
assertThat(result, is(6765));
}
@Test
public void shouldCompileConditionalPattern() {
int result = exec(
"module scotch.test",
"",
"run = fib 20",
"fib 0 = 0",
"fib 1 = 1",
"fib n = fib (n - 1) + fib (n - 2)"
);
assertThat(result, is(6765));
}
@Test
public void shouldCompileDataDeclaration() throws ReflectiveOperationException {
Object result = exec(
"module scotch.test",
"data Maybe a = Nothing | Just a",
"run = Just \"Waffles\""
);
Method getter = result.getClass().getMethod("get_0");
assertThat(((Callable) getter.invoke(result)).call(), is("Waffles"));
}
@Test
public void equivalentDataShouldBeEqual() {
boolean result = exec(
"module scotch.test",
"import scotch.java",
"data Thing a { value :: a }",
"run = Thing 2 `javaEq?!` Thing 2"
);
assertThat(result, is(true));
}
@Test
public void equivalentDataShouldHaveSameHashCode() {
boolean result = exec(
"module scotch.test",
"import scotch.java",
"",
"data Thing n { value :: n }",
"",
"run = (javaHash! $ Thing 2) == (javaHash! $ Thing 2)"
);
assertThat(result, is(true));
}
@Test
public void shouldCreateDataFromInitializerWithArbitrarilyOrderedFields() {
boolean result = exec(
"module scotch.test",
"import scotch.java",
"",
"data QuantifiedThing a { howMany :: Int, what :: a }",
"",
"run = QuantifiedThing { howMany = 32, what = \"Bananas\" } `javaEq?!`",
" QuantifiedThing { what = \"Bananas\", howMany = 32 }"
);
assertThat(result, is(true));
}
@Test
public void shouldCompileParenthesizedSignature() {
exec(
"module scotch.test",
"import scotch.java",
"",
"data Thing n { value :: n }",
"",
"($) :: (a -> b) -> a -> b",
"right infix 0 ($)",
"fn $ arg = fn arg",
"",
"run = (javaHash! $ Thing 2) == (javaHash! $ Thing 2)"
);
}
@Test
public void shouldCompileBind() {
Left result = exec(
"module scotch.test",
"run = Right \"Yes\" >>= \\which -> Left 0"
);
assertThat(result, is(left(0)));
}
@Test
public void shouldCompileDoNotation() {
Maybe result = exec(
"module scotch.test",
"",
"run = do",
" val <- Just 3",
" return $ val + 2"
);
assertThat(result, is(just(5)));
}
@Test
public void shouldGetJust5() {
Maybe<Integer> result = exec(
"module scotch.test",
"import scotch.data.num", // TODO should not require import
"",
"run = do",
" x <- Just 3",
" y <- Just 2",
" return $ x + y"
);
assertThat(result, is(just(5)));
}
@Test
public void shouldCompileTupleLiteral() {
Tuple3<Integer, Integer, Tuple2<Integer, Integer>> tuple = exec(
"module scotch.test",
"run = (1, 2, (3, 4))"
);
assertThat(tuple, is(tuple3(1, 2, tuple2(3, 4))));
}
@Test
public void listsShouldEqual() {
boolean result = exec(
"module scotch.test",
"run = [1, 2, 3] == [1, 2, 3]"
);
assertThat(result, is(true));
}
@Test
public void emptyListsShouldEqual() {
boolean result = exec(
"module scotch.test",
"run = [] == []"
);
assertThat(result, is(true));
}
@Test
public void listShouldEqualConsList() {
boolean result = exec(
"module scotch.test",
"run = [1, 2, 3] == 1:2:3:[]"
);
assertThat(result, is(true));
}
@Test
public void shouldParseIgnoredPattern() {
int result = exec(
"module scotch.test",
"fn = \\_ -> 2",
"run = fn 3"
);
assertThat(result, is(2));
}
@Ignore
@Test
public void shouldCompileShow() {
String result = exec(
"module scotch.test",
"import scotch.text.show",
"import scotch.java",
"instance Show Int where",
" show = jIntShow",
"run = show 5"
);
assertThat(result, is("5"));
}
@Test
public void shouldCreatePickleWithEnumConstants() {
Object pickle = exec(
"module scotch.test",
"",
"data Texture = Soft | Crunchy",
"data Pickle { kind :: Texture, pimples :: Int }",
"pickle = Pickle Crunchy 15",
"run = pickle"
);
assertThat(pickle.toString(), is("Pickle { kind = Crunchy, pimples = 15 }"));
}
@Test
public void shouldCreateThing() {
Object thing = exec(
"module scotch.test",
"data Thing n { value :: n }",
"run = Thing \"Toast\""
);
assertThat(thing.toString(), is("Thing { value = Toast }"));
}
@Test
public void shouldGetOrdering() {
boolean shouldBeTruthy = exec(
"module scotch.test",
"",
"run = max 2 3 == 3 && max 2 3 == max 3 2",
" && min 2 3 == 2 && min 2 3 == min 3 2",
" && 2 < 3",
" && 3 > 2",
" && 2 <= 3 && 2 <= 2",
" && 3 >= 2 && 3 >= 3",
" && LessThan == compare 2 3",
" && GreaterThan == compare 3 2",
" && EqualTo == compare 2 2"
);
assertThat(shouldBeTruthy, is(true));
}
@Test
public void shouldDestructureTuple() {
int value = exec(
"module scotch.test",
"second (_, b) = b",
"run = second (3, 2)"
);
assertThat(value, is(2));
}
@Test
public void shouldDestructureNestedTuple() {
int value = exec(
"module scotch.test",
"third (_, (_, c)) = c",
"run = third (1, (5, 3))"
);
assertThat(value, is(3));
}
@Test
public void shouldDestructureDeeplyNestedTuple() {
int value = exec(
"module scotch.test",
"secondOfSecondOfFirst ((_, (a, _)), _) = a",
"run = secondOfSecondOfFirst ((1, (2, 3)), 4)"
);
assertThat(value, is(2));
}
@Test
public void shouldNegateNumber() {
int value = exec(
"module scotch.test",
"run = -4"
);
assertThat(value, is(-4));
}
@Test
public void shouldDestructureToast() {
boolean burned = exec(
"module scotch.test",
"data Toast { kind :: String, burnLevel :: Int }",
"isBurned? Toast { burnLevel = b } = b > 3",
"run = isBurned? Toast { burnLevel = 4, kind = \"Wheat\" }"
);
assertThat(burned, is(true));
}
@Test
public void shouldDestructurePerson() {
String firstName = exec(
"module scotch.test",
"import scotch.data.tuple", // TODO should not require import
"data Person { name :: (String, String) }",
"firstName Person { name = (fn, _) } = fn",
"run = firstName Person { name = (\"Alice\", \"Wonderland\") }"
);
assertThat(firstName, is("Alice"));
}
@Test
public void shouldDestructureWithMultipleCases() {
boolean newborn = exec(
"module scotch.test",
"data Person { age :: Int }",
"newborn? Person { age = 0 } = True",
"newborn? Person { age = _ } = False",
"run = newborn? Person { age = 1 }"
);
assertThat(newborn, is(false));
}
@Test
public void shouldGetFieldImplicitly() {
String toastKind = exec(
"module scotch.test",
"data Toast { kind :: String, burnLevel :: Int }",
"kind Toast { kind } = kind",
"run = kind Toast { burnLevel = 2, kind = \"Rye\" }"
);
assertThat(toastKind, is("Rye"));
}
@Test
public void shouldGetHeadOfList() {
int head = exec(
"module scotch.test",
"head (x:_) = x",
"run = head [1, 2, 3]"
);
assertThat(head, is(1));
}
@Test
public void shouldGetTailOfList() {
ConsList<Integer> tail = exec(
"module scotch.test",
"tail (_:xs) = xs",
"run = tail [1, 2, 3]"
);
assertThat(tail, is(eagerCons(2, 3)));
}
@Test
public void shouldGetSecondTail() {
ConsList<Integer> secondTail = exec(
"module scotch.test",
"secondTail (_:_:xs) = xs",
"run = secondTail [1, 2, 3, 4]"
);
assertThat(secondTail, is(eagerCons(3, 4)));
}
@Test
public void shouldNotBeEmptyList() {
boolean empty = exec(
"module scotch.test",
"empty? [] = True",
"empty? _ = False",
"run = empty? [1, 2]"
);
assertThat(empty, is(false));
}
@Test
public void shouldBeEmptyList() {
boolean empty = exec(
"module scotch.test",
"empty? [] = True",
"empty? _ = False",
"run = empty? []"
);
assertThat(empty, is(true));
}
@Test
public void shouldStartWithTwo() {
boolean startsWithTwo = exec(
"module scotch.test",
"startsWithTwo? (2:_) = True",
"startsWithTwo? _ = False",
"run = startsWithTwo? [2, 3, 4]"
);
assertThat(startsWithTwo, is(true));
}
@Test
public void shouldNotStartWithTwo() {
boolean startsWithTwo = exec(
"module scotch.test",
"startsWithTwo? (2:_) = True",
"startsWithTwo? _ = False",
"run = startsWithTwo? [1, 2, 3]"
);
assertThat(startsWithTwo, is(false));
}
@Test
public void shouldGetBool() {
boolean fiveIsTrue = exec(
"module scotch.test",
"",
"five? :: Int -> Bool",
"five? n = n == 5",
"run = five? 5"
);
assertThat(fiveIsTrue, is(true));
}
@Test(expected = RaisedException.class)
public void shouldRaiseError() {
exec(
"module scotch.test",
"run = raise \"Oops!\""
);
}
@Test
public void shouldDestructureWithLiteralFunction() {
int pimples = exec(
"module scotch.test",
"",
"data Texture = Soft | Crunchy",
"data Pickle { kind :: Texture, pimples :: Int }",
"",
"pimples = \\Pickle { pimples = p } -> p",
"run = pimples Pickle { kind = Crunchy, pimples = 23 }"
);
assertThat(pimples, is(23));
}
@Test
public void shouldDestructureTupleWithLiteralFunction() {
int second = exec(
"module scotch.test",
"run = (\\(_, b) -> b) (1, 2)"
);
assertThat(second, is(2));
}
@Test
public void shouldTestEqualityBetweenDoubles() {
boolean equal = exec(
"module scotch.test",
"run = 2.2 == 2.2"
);
assertThat(equal, is(true));
}
@Test
public void shouldAddDoubles() {
double value = exec(
"module scotch.test",
"run = 2.2 + 2.2"
);
assertThat(value, is(4.4));
}
@Test
public void shouldCompareDoubles() {
boolean greaterThan = exec(
"module scotch.test",
"run = 2.2 > 2.3"
);
assertThat(greaterThan, is(false));
}
@Test
public void shouldConcatenateStrings() {
String result = exec(
"module scotch.test",
"hello = \"Hello\"",
"world = \"World\"",
"run = hello ++ \" \" ++ world ++ \"!\""
);
assertThat(result, is("Hello World!"));
}
@Test
public void shouldShowDouble() {
String result = exec(
"module scotch.test",
"import scotch.text.show",
"run = \"value: \" ++ show 2.2"
);
assertThat(result, is("value: 2.2"));
}
@Test
public void shouldShowInt() {
String result = exec(
"module scotch.test",
"import scotch.text.show",
"run = \"value: \" ++ show 2"
);
assertThat(result, is("value: 2"));
}
@Test
public void shouldShowList() {
String result = exec(
"module scotch.test",
"import scotch.text.show",
"run = \"value: \" ++ show [1, 2, 3]"
);
assertThat(result, is("value: [1, 2, 3]"));
}
@Test
public void shouldGetRatio() {
Ratio<Integer> result = exec(
"module scotch.test",
"import scotch.data.ratio",
"run = 1 % 3"
);
assertThat(result, is(ratio(1, 3)));
}
@Test
public void shouldConvertRatioToDouble() {
double result = exec(
"module scotch.test",
"import scotch.data.ratio",
"run = toDouble $ 1 % 2"
);
assertThat(result, is(0.5));
}
@Test
public void shouldDivide() {
double result = exec(
"module scotch.test",
"import scotch.data.fractional",
"run = 4.0 / 2.0"
);
assertThat(result, is(2.0));
}
@Test
public void shouldGetSquareRoot() {
double result = exec(
"module scotch.test",
"import scotch.data.floating",
"run = sqrt 25.0"
);
assertThat(result, is(5.0));
}
@Test
public void ratiosShouldEqual() {
boolean equal = exec(
"module scotch.test",
"import scotch.data.ratio",
"run = 3 % 2 == 3 % 2"
);
assertThat(equal, is(true));
}
@Test
public void shouldGetPowerOfThree() {
double result = exec(
"module scotch.test",
"import scotch.data.floating",
"run = 9.0 ** 3.0"
);
assertThat(result, is(729.0));
}
@Test
public void shouldConvertDoubleToInt() {
int result = exec(
"module scotch.test",
"run = toInt 3524578.0000000037"
);
assertThat(result, is(3524578));
}
@Test
public void fourModTwoShouldBeZero() {
boolean result = exec(
"module scotch.test",
"run = 4 `intMod` 2 == 0"
);
assertThat(result, is(true));
}
@Test
public void shouldFilterList() {
ConsList<Integer> list = exec(
"module scotch.test",
"",
"filter predicate (x:xs) = if predicate x",
" then x : filter predicate xs",
" else filter predicate xs",
"filter predicate [] = []",
"",
"run = filter (\\n -> n `intMod` 2 == 0) [1, 2, 3, 4, 5]"
);
assertThat(list, equalTo(eagerCons(2, 4)));
}
@Test
public void shouldCompileFunctionWithAngleBracketInName() {
int result = exec(
"module scotch.test",
"left infix 0 ($>)",
"a $> fn = fn a",
"fn x = x",
"run = 1 $> fn"
);
assertThat(result, is(1));
}
@Test
public void shouldPipeForward() {
int result = exec(
"module scotch.test",
"id x = x",
"run = 1 |> id"
);
assertThat(result, is(1));
}
@Test
public void shouldAddBigInts() {
BigInteger result = exec(
"module scotch.test",
"run = 2b + 4b"
);
assertThat(result, is(new BigInteger("6")));
}
@Test
public void bigIntegersShouldEqual() {
boolean result = exec(
"module scotch.test",
"run = 2b + 5b == 7b"
);
assertThat(result, is(true));
}
@Test
public void shouldCompleGuardCase() {
ConsList<Integer> result = exec(
"module scotch.test",
"run = filter (\\x -> x `intMod` 2 == 0) [1, 2, 3]",
"filter predicate (x:xs) | predicate x = x : filter predicate xs",
" | otherwise = filter predicate xs",
"filter predicate [] = []"
);
assertThat(result, is(eagerCons(2)));
}
@Test
public void shouldExpectHeadOfListToBe1() {
exec(
"module scotch.test",
"import scotch.text.show",
"import scotch.data.eq",
"head (x:xs) = x",
"head _ = raise \"Can't get head of empty list!\"",
"run = expect (head [1, 2, 3]) toBe 1"
);
}
@Test
public void shouldExpectHeadOfEmptyListToRaise() {
exec(
"module scotch.test",
"head (x:xs) = x",
"head _ = raise \"Can't get head of empty list!\"",
"run = expect (head []) toRaise \"Can't get head of empty list!\""
);
}
@SuppressWarnings("unchecked")
private <A> A exec(String... lines) {
try {
ClassLoaderResolver resolver = new ClassLoaderResolver(
Optional.of(new File("build/generated-test-classes/" + testName.getMethodName())),
scotch.compiler.Compiler.class.getClassLoader()
);
resolver.defineAll(TestUtil.generateBytecode(resolver, lines));
return ((Callable<A>) resolver.loadClass("scotch.test.$$Module").getMethod("run").invoke(null)).call();
} catch (ReflectiveOperationException exception) {
throw new RuntimeException(exception);
}
}
}