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)));
}
}