package scotch.compiler.parser; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static me.qmx.jitescript.util.CodegenUtils.ci; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import static org.objectweb.asm.Opcodes.ACC_FINAL; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_STATIC; import static scotch.compiler.syntax.reference.DefinitionReference.moduleRef; import static scotch.compiler.syntax.reference.DefinitionReference.rootRef; import static scotch.compiler.syntax.value.Values.apply; import static scotch.util.TestUtil.arg; import static scotch.util.TestUtil.capture; import static scotch.util.TestUtil.classDef; import static scotch.util.TestUtil.classRef; import static scotch.util.TestUtil.conditional; import static scotch.util.TestUtil.constantRef; import static scotch.util.TestUtil.constantValue; import static scotch.util.TestUtil.construct; import static scotch.util.TestUtil.ctorDef; import static scotch.util.TestUtil.dataDef; import static scotch.util.TestUtil.dataRef; import static scotch.util.TestUtil.equal; import static scotch.util.TestUtil.field; import static scotch.util.TestUtil.fieldDef; import static scotch.util.TestUtil.fn; import static scotch.util.TestUtil.id; import static scotch.util.TestUtil.ignore; import static scotch.util.TestUtil.initializer; import static scotch.util.TestUtil.literal; import static scotch.util.TestUtil.matcher; import static scotch.util.TestUtil.operatorDef; import static scotch.util.TestUtil.operatorRef; import static scotch.util.TestUtil.pattern; import static scotch.util.TestUtil.root; import static scotch.util.TestUtil.scopeRef; import static scotch.util.TestUtil.signatureRef; import static scotch.util.TestUtil.struct; import static scotch.util.TestUtil.unshuffled; import static scotch.util.TestUtil.unshuffledMatch; import static scotch.util.TestUtil.valueRef; import static scotch.control.monad.Monad.fail; import static scotch.symbol.FieldSignature.fieldSignature; import static scotch.symbol.Value.Fixity.LEFT_INFIX; import static scotch.symbol.Value.Fixity.PREFIX; import static scotch.compiler.syntax.type.Types.ctor; import static scotch.compiler.syntax.type.Types.fn; import static scotch.compiler.syntax.type.Types.sum; import static scotch.compiler.syntax.type.Types.t; import static scotch.compiler.syntax.type.Types.var; import static scotch.compiler.text.TextUtil.quote; import java.util.List; import java.util.Optional; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import scotch.compiler.syntax.definition.DataTypeDefinition; import scotch.compiler.syntax.definition.Definition; import scotch.compiler.syntax.definition.DefinitionGraph; import scotch.compiler.syntax.definition.ValueDefinition; import scotch.compiler.syntax.definition.ValueSignature; import scotch.compiler.syntax.pattern.PatternMatch; import scotch.compiler.syntax.reference.DefinitionReference; import scotch.compiler.syntax.value.Value; import scotch.runtime.Callable; import scotch.symbol.SymbolResolver; import scotch.symbol.Value.Fixity; import scotch.compiler.syntax.type.Type; import scotch.compiler.syntax.util.DefaultSymbolGenerator; import scotch.compiler.syntax.util.SymbolGenerator; public class SyntaxTransformerTest extends ScotchParserBaseTest { @Rule public final ExpectedException exception = ExpectedException.none(); private DefinitionGraph graph; private SymbolGenerator symbolGenerator; private SymbolResolver symbolResolver; @Before public void setUp() { symbolGenerator = new DefaultSymbolGenerator(); symbolResolver = mock(SymbolResolver.class); } @Test public void shouldTransformRoot() { transform( "module a.b.c", "module x.y.z" ); shouldHaveDefinition(rootRef(), root(asList( moduleRef("a.b.c"), moduleRef("x.y.z") ))); } @Test public void shouldTransformOperatorDefinitions() { transform( "module scotch.test", "left infix 7 (*), (/), (%)", "left infix 6 (+), (-)", "prefix 9 `not`" ); shouldHaveOperator("scotch.test.(*)", LEFT_INFIX, 7); shouldHaveOperator("scotch.test.(/)", LEFT_INFIX, 7); shouldHaveOperator("scotch.test.(%)", LEFT_INFIX, 7); shouldHaveOperator("scotch.test.(+)", LEFT_INFIX, 6); shouldHaveOperator("scotch.test.(-)", LEFT_INFIX, 6); shouldHaveOperator("scotch.test.not", PREFIX, 9); } @Test public void shouldTransformExpressionsToUnshuffledValues() { transform( "module scotch.test", "value = fn (a b)" ); shouldHaveValue("scotch.test.value", unshuffled( id("fn", t(0)), unshuffled(id("a", t(1)), id("b", t(2))) )); } @Test public void shouldTransformPatternSignature() { transform( "module scotch.test", "length :: String -> Int" ); shouldHaveSignature("scotch.test.length", fn(sum("String"), sum("Int"))); } @Test public void shouldTransformPatternSignatureWithTuple() { transform( "module scotch.test", "second :: (a, b) -> b" ); shouldHaveSignature("scotch.test.second", fn(sum("scotch.data.tuple.(,)", asList(var("a"), var("b"))), var("b"))); } @Test public void shouldTransformPatternSignatureWithList() { transform( "module scotch.test", "head :: [a] -> a" ); shouldHaveSignature("scotch.test.head", fn(sum("scotch.data.list.[]", asList(var("a"))), var("a"))); } @Test public void shouldTransformPatternSignatureWithParameterizedSum() { transform( "module scotch.test", "find :: Map a b -> a -> Maybe b" ); shouldHaveSignature("scotch.test.find", fn(sum("Map", asList(var("a"), var("b"))), fn(var("a"), sum("Maybe", var("b"))))); } @Test public void shouldTransformPatternSignatureWithParameterizedVariable() { transform( "module scotch.test", "(>>=) :: m a -> (a -> m b) -> m b" ); shouldHaveSignature("scotch.test.(>>=)", fn(ctor(var("m"), var("a")), fn(fn(var("a"), ctor(var("m"), var("b"))), ctor(var("m"), var("b"))))); } @Test public void shouldTransformPatternSignatureWithTypeContext() { transform( "module scotch.test", "(==) :: Eq a => a -> a -> Bool" ); shouldHaveSignature("scotch.test.(==)", fn(var("a", asList("Eq")), fn(var("a", asList("Eq")), sum("Bool")))); } @Test public void shouldTransformPatternSignatureWithMultipleTypeContexts() { transform( "module scotch.test", "value :: (Eq a, Num a) => a" ); shouldHaveSignature("scotch.test.value", var("a", asList("Eq", "Num"))); } @Test public void shouldTransformFunctionLiteral() { transform( "module scotch.test", "id = \\x -> x" ); shouldHaveValue("scotch.test.id", matcher("scotch.test.(id#0)", t(0), arg("#0", t(2)), pattern("scotch.test.(id#0#0)", asList(capture("x", t(1))), unshuffled(id("x", t(3)))))); } @Test public void shouldTransformMultiArgFunctionLiteral() { transform( "module scotch.test", "apply = \\x y -> x y" ); shouldHaveValue("scotch.test.apply", matcher("scotch.test.(apply#0)", t(0), asList(arg("#0", t(3)), arg("#1", t(4))), pattern("scotch.test.(apply#0#0)", asList(capture("x", t(1)), capture("y", t(2))), unshuffled(id("x", t(5)), id("y", t(6)))))); } @Test public void shouldTransformIgnoringFunctionLiteral() { transform( "module scotch.test", "fn = \\_ -> 3" ); shouldHaveValue("scotch.test.fn", matcher("scotch.test.(fn#0)", t(0), arg("#0", t(2)), pattern("scotch.test.(fn#0#0)", asList(ignore(t(1))), unshuffled(literal(3))))); } @Test public void shouldTransformConditional() { transform( "module scotch.test", "really? = if True then \"Yes\"", " else if Maybe then \"Maybe\"", " else \"Nope\"" ); shouldHaveValue("scotch.test.(really?)", conditional( unshuffled(literal(true)), unshuffled(literal("Yes")), unshuffled(conditional( unshuffled(id("Maybe", t(0))), unshuffled(literal("Maybe")), unshuffled(literal("Nope")), t(1) )), t(2) )); } @Test public void shouldTransformInitializer() { transform( "module scotch.test", "toast = Toast {", " type = Rye, butter = Yes, jam = No", "}" ); shouldHaveValue("scotch.test.toast", initializer(t(1), id("Toast", t(0)), asList( field("type", unshuffled(id("Rye", t(2)))), field("butter", unshuffled(id("Yes", t(3)))), field("jam", unshuffled(id("No", t(4)))) ))); } @Test public void shouldTransformDoNotationWithThen() { transform( "module scotch.test", "messaged = do", " println \"Hello World!\"", " println \"Debilitating coffee addiction\"" ); shouldHaveValue("scotch.test.messaged", unshuffled( unshuffled(id("println", t(1)), literal("Hello World!")), id("scotch.control.monad.(>>)", t(2)), unshuffled(id("println", t(0)), literal("Debilitating coffee addiction")) )); } @Test public void shouldTransformsDoNotationWithBind() { transform( "module scotch.test", "pingpong = do", " ping <- readln", " println (\"ponging back! \" ++ ping)" ); shouldHaveValue("scotch.test.pingpong", unshuffled( unshuffled(id("readln", t(1))), id("scotch.control.monad.(>>=)", t(5)), fn("scotch.test.(pingpong#0)", arg("ping", t(0)), unshuffled( id("println", t(2)), unshuffled(literal("ponging back! "), id("++", t(3)), id("ping", t(4))) )) )); } @Test public void shouldThrow_whenBindIsLastLineInDoNotation() { exception.expectMessage(containsString("Unexpected bind in do-notation")); transform( "module scotch.test", "pingpong = do", " ping <- readln", " println (\"ponging back! \" ++ ping)", " pong <- readln" ); fail(); } @Test public void shouldParseMultipleDrawFromsInDoNotation() { transform( "module scotch.test", "addedStuff = do", " x <- Just 3", " y <- Just 2", " return $ x + y" ); shouldHaveValue("scotch.test.addedStuff", unshuffled( unshuffled( id("Just", t(1)), literal(3) ), id("scotch.control.monad.(>>=)", t(10)), fn("scotch.test.(addedStuff#0)", arg("x", t(0)), unshuffled( unshuffled( id("Just", t(3)), literal(2) ), id("scotch.control.monad.(>>=)", t(9)), fn("scotch.test.(addedStuff#1)", arg("y", t(2)), unshuffled( id("return", t(4)), id("$", t(5)), id("x", t(6)), id("+", t(7)), id("y", t(8)) )) )) )); } @Test public void shouldTransformTupleLiteral() { transform( "module scotch.test", "tuple = (1, 2, 3)" ); shouldHaveValue("scotch.test.tuple", initializer(t(0), id("scotch.data.tuple.(,,)", t(1)), asList( field("_0", unshuffled(literal(1))), field("_1", unshuffled(literal(2))), field("_2", unshuffled(literal(3))) ))); } @Test public void shouldTransformListLiteral() { transform( "module scotch.test", "list = [1, 2]" ); shouldHaveValue("scotch.test.list", initializer(t(3), id("scotch.data.list.(:)", t(4)), asList( field("_0", unshuffled(literal(1))), field("_1", initializer(t(1), id("scotch.data.list.(:)", t(2)), asList( field("_0", unshuffled(literal(2))), field("_1", constantValue("scotch.data.list.[]", "scotch.data.list.[]", t(0))) ))) ))); } @Test public void shouldTransformTupleDestructuringPattern() { transform( "module scotch.test", "second (_, b) = b" ); shouldHavePattern("scotch.test.(#0)", asList(capture("second", t(0)), struct("scotch.data.tuple.(,)", t(1), asList( field("_0", t(4), ignore(t(3))), field("_1", t(7), capture("b", t(6)))))), unshuffled(id("b", t(8))) ); } @Test public void shouldTransformListDestructuringPattern() { transform( "module scotch.test", "tail (_:xs) = xs" ); shouldHavePattern("scotch.test.(#0)", asList(capture("tail", t(0)), unshuffledMatch(t(1), ignore(t(2)), equal(id(":", t(3))), capture("xs", t(4)))), unshuffled(id("xs", t(5)))); } @Test public void shouldParseParenthesizedCapturePattern() { transform( "module scotch.test", "second (_, (b)) = b" ); shouldHavePattern("scotch.test.(#0)", asList(capture("second", t(0)), struct("scotch.data.tuple.(,)", t(1), asList( field("_0", t(4), ignore(t(3))), field("_1", t(8), capture("b", t(7)))))), unshuffled(id("b", t(9))) ); } @Test public void shouldTransformClassDefinition() { transform( "module scotch.data.eq", "prefix 4 `not`", "left infix 5 (==), (/=)", "class Eq a where", " (==), (/=) :: a -> a -> Bool", " x == y = `not` x /= y", " x /= y = `not` x == y" ); shouldHaveClass("scotch.data.eq.Eq", asList(var("a")), asList( signatureRef("scotch.data.eq.(==)"), signatureRef("scotch.data.eq.(/=)"), scopeRef("scotch.data.eq.(#0)"), scopeRef("scotch.data.eq.(#1)") )); } @Test public void shouldTransformUnaryDataDeclarationWithNamedFields() { transform( "module scotch.test", "data Toast {", " type :: Bread,", " butter :: Bool,", " jam :: Bool", "}" ); shouldHaveDataType("scotch.test.Toast", dataDef("scotch.test.Toast", emptyList(), asList( ctorDef(0, "scotch.test.Toast", "scotch.test.Toast", asList( fieldDef(0, "type", sum("Bread")), fieldDef(1, "butter", sum("Bool")), fieldDef(2, "jam", sum("Bool")) )) ))); } @Test public void shouldCreateDataConstructorValue() { transform( "module scotch.test", "data Toast {", " type :: Bread,", " butter :: Bool,", " jam :: Bool", "}" ); shouldHaveValue("scotch.test.Toast", fn( "scotch.test.(#0)", asList(arg("type", sum("Bread")), arg("butter", sum("Bool")), arg("jam", sum("Bool"))), construct("scotch.test.Toast", sum("scotch.test.Toast"), asList( id("type", sum("Bread")), id("butter", sum("Bool")), id("jam", sum("Bool")))))); } @Test public void shouldCreateConstantForNiladicConstructor() { transform( "module scotch.test", "data Maybe a = Nothing | Just a" ); shouldHaveValue("scotch.test.Nothing", constantRef( "scotch.test.Nothing", "scotch.test.Maybe", fieldSignature("scotch/test/Maybe$Nothing", ACC_STATIC | ACC_PUBLIC | ACC_FINAL, "INSTANCE", ci(Callable.class)), sum("scotch.test.Maybe", var("a")) )); } @Test public void shouldCreateDataConstructorWithAnonymousFields() { transform( "module scotch.test", "data Maybe a = Nothing | Just a" ); shouldHaveValue("scotch.test.Just", fn( "scotch.test.(#0)", asList(arg("_0", var("a"))), construct("scotch.test.Just", sum("scotch.test.Maybe", var("a")), asList( id("_0", var("a")))))); } @Test public void shouldDestructureToast() { transform( "module scotch.test", "data Toast { kind :: String, burnLevel :: Int }", "isBurned? Toast { burnLevel = b } = b > 3" ); shouldHavePattern("scotch.test.(#1)", asList( capture("isBurned?", t(0)), struct("Toast", t(1), asList( field("burnLevel", t(2), capture("b", t(4))) )) ), unshuffled(id("b", t(5)), id(">", t(6)), literal(3)) ); } @Test public void shouldDestructureToastWithImplicitFieldCapture() { transform( "module scotch.test", "data Toast { kind :: String, burnLevel :: Int }", "isBurned? Toast { burnLevel } = burnLevel > 3" ); shouldHavePattern("scotch.test.(#1)", asList( capture("isBurned?", t(0)), struct("Toast", t(1), asList( field("burnLevel", t(2), capture("burnLevel", t(2))) )) ), unshuffled(id("burnLevel", t(3)), id(">", t(4)), literal(3)) ); } @Test public void shouldDestructureToastWithTupleFieldMatch() { transform( "module scotch.test", "data Person { name :: (String, String) }", "firstName Person { name = (fn, _) } = fn" ); shouldHavePattern("scotch.test.(#1)", asList( capture("firstName", t(0)), struct("Person", t(1), asList( field("name", t(2), struct("scotch.data.tuple.(,)", t(4), asList( field("_0", t(7), capture("fn", t(6))), field("_1", t(10), ignore(t(9))) ))) )) ), unshuffled(id("fn", t(11)))); } @Test public void shouldTransformLiteralPatternMatch() { transform( "module scotch.test", "fib 0 = 0" ); shouldHavePattern("scotch.test.(#0)", asList(capture("fib", t(0)), equal(literal(0))), unshuffled(literal(0))); } @Test public void shouldTransformGuardCase() { transform( "module scotch.test", "fn x | True = 0", " | False = 1" ); shouldHavePattern("scotch.test.(#0)", asList(capture("fn", t(0)), capture("x", t(1))), conditional( unshuffled(literal(true)), unshuffled(literal(0)), conditional( unshuffled(literal(false)), unshuffled(literal(1)), apply(id("scotch.lang.raise", t(2)), literal("Incomplete guard"), t(3)), t(4) ), t(5) )); } private Optional<Definition> getDefinition(DefinitionReference reference) { return graph.getDefinition(reference); } protected Optional<ValueDefinition> getValueDefinition(String name) { return graph.getDefinition(valueRef(name)); } private void shouldHaveClass(String className, List<Type> arguments, List<DefinitionReference> members) { assertThat(graph.getDefinition(classRef(className)).get(), is( classDef(className, arguments, members) )); } private void shouldHaveDataType(String name, DataTypeDefinition value) { assertThat(graph.getDefinition(dataRef(name)), is(Optional.of(value))); } private void shouldHaveDefinition(DefinitionReference reference, Definition definition) { assertThat(getDefinition(reference), is(Optional.of(definition))); } private void shouldHaveOperator(String name, Fixity fixity, int precedence) { shouldHaveDefinition(operatorRef(name), operatorDef(name, fixity, precedence)); } private void shouldHavePattern(String name, List<PatternMatch> matches, Value body) { assertThat(getDefinition(scopeRef(name)), is(Optional.of(unshuffled(name, matches, body)))); } protected void shouldHaveValue(String name) { assertThat("Graph did not define value " + quote(name), graph.getValue(valueRef(name)).isPresent(), is(true)); } protected void shouldHaveValue(String name, Value body) { shouldHaveValue(name); assertThat(getValueDefinition(name).map(ValueDefinition::getBody), is(Optional.of(body))); } private void transform(String... data) { graph = new SyntaxTransformer(symbolResolver, symbolGenerator).transform(parseModules(data)); } protected void shouldHaveSignature(String name, Type type) { assertThat(getDefinition(signatureRef(name)).map(s -> (ValueSignature) s).map(ValueSignature::getType), is(Optional.of(type))); } }