aboutsummaryrefslogtreecommitdiff
path: root/core/src/test/java/coffee
diff options
context:
space:
mode:
Diffstat (limited to 'core/src/test/java/coffee')
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java11
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java11
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java5
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java3
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java3
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java97
-rw-r--r--core/src/test/java/coffee/liz/ecs/DAGWorldTest.java83
-rw-r--r--core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java60
8 files changed, 221 insertions, 52 deletions
diff --git a/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java b/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java
index d87204d..ca1ee9c 100644
--- a/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java
+++ b/core/src/test/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngineTest.java
@@ -351,6 +351,17 @@ public class VimEngineTest {
}
@Test
+ public void characterPasteOnEmptyLineInsertsIntoThatLine() {
+ final FakeBuffer buffer = new FakeBuffer("cat\n\ndog", 0);
+ final VimEngine controller = new VimEngine(buffer, status -> {
+ });
+
+ type(controller, 'y', 'w', 'j', 'p');
+
+ assertEquals("cat\ncat\ndog", buffer.getText());
+ }
+
+ @Test
public void dwDeletesWord() {
final FakeBuffer buffer = new FakeBuffer("one two", 0);
final VimEngine controller = new VimEngine(buffer, status -> {
diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java
index 9f1ff41..ffee701 100644
--- a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java
+++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystemTest.java
@@ -15,6 +15,7 @@ import coffee.liz.abstractionengine.grid.component.GridPosition;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.Entity;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import lombok.Getter;
import org.junit.jupiter.api.Test;
@@ -59,7 +60,7 @@ final class GridCollisionPropagatationSystemTest {
final Entity wall = Entity.builder().id(3).build().add(WALL_COLLIDABLE).add(new GridPosition(Vec2i.EAST));
// Propagation takes priority because priority(0) is lower
- when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class)))
+ when(world.resolve(Query.allOf(GridMomentum.class, GridPosition.class, GridCollidable.class)))
.thenReturn(Set.of(pusher));
when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher));
@@ -86,7 +87,7 @@ final class GridCollisionPropagatationSystemTest {
final Entity wall = Entity.builder().id(3).build().add(WALL_COLLIDABLE)
.add(new GridPosition(Vec2i.EAST.scale(2, 0)));
- when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class)))
+ when(world.resolve(Query.allOf(GridMomentum.class, GridPosition.class, GridCollidable.class)))
.thenReturn(Set.of(pusher, toPropagate));
when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher));
@@ -111,7 +112,7 @@ final class GridCollisionPropagatationSystemTest {
final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.EAST))
.add(new GridPosition(Vec2i.ZERO));
- when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class)))
+ when(world.resolve(Query.allOf(GridMomentum.class, GridPosition.class, GridCollidable.class)))
.thenReturn(Set.of(pusher));
when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher));
@@ -133,7 +134,7 @@ final class GridCollisionPropagatationSystemTest {
final Entity pusher = Entity.builder().id(1).build().add(PROPAGATE_COLLIDABLE).add(new GridMomentum(Vec2i.ZERO))
.add(new GridPosition(Vec2i.ZERO));
- when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class)))
+ when(world.resolve(Query.allOf(GridMomentum.class, GridPosition.class, GridCollidable.class)))
.thenReturn(Set.of(pusher));
when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher));
@@ -160,7 +161,7 @@ final class GridCollisionPropagatationSystemTest {
final Entity swallower = Entity.builder().id(2).build().add(swallowCollidable)
.add(new GridPosition(Vec2i.EAST));
- when(world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class)))
+ when(world.resolve(Query.allOf(GridMomentum.class, GridPosition.class, GridCollidable.class)))
.thenReturn(Set.of(pusher));
when(indexSystem.entitiesAt(Vec2i.ZERO)).thenReturn(List.of(pusher));
diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java
index f751927..9fd1a7c 100644
--- a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java
+++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridIndexSystemTest.java
@@ -9,6 +9,7 @@ import coffee.liz.abstractionengine.grid.component.GridPosition;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.Entity;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -25,7 +26,7 @@ class GridIndexSystemTest {
alpha.add(new GridPosition(new Vec2i(1, 2)));
final Entity beta = Entity.builder().id(12).build();
beta.add(new GridPosition(new Vec2i(0, 0)));
- when(world.query(Set.of(GridPosition.class))).thenReturn(Set.of(alpha, beta));
+ when(world.resolve(Query.allOf(GridPosition.class))).thenReturn(Set.of(alpha, beta));
system.update(world, GridInputState.builder().movement(Vec2i.NORTH).build(), Duration.ZERO);
@@ -40,7 +41,7 @@ class GridIndexSystemTest {
final World<GridInputState> world = mockWorld();
final Entity moving = Entity.builder().id(77).build();
moving.add(new GridPosition(Vec2i.ZERO));
- when(world.query(Set.of(GridPosition.class))).thenReturn(Set.of(moving)).thenReturn(Set.of());
+ when(world.resolve(Query.allOf(GridPosition.class))).thenReturn(Set.of(moving)).thenReturn(Set.of());
system.update(world, GridInputState.builder().movement(Vec2i.EAST).build(), Duration.ZERO);
assertTrue(system.entitiesAt(Vec2i.ZERO).contains(moving));
diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java
index 2731a7b..0c4ad80 100644
--- a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java
+++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridMovementSystemTest.java
@@ -10,6 +10,7 @@ import coffee.liz.abstractionengine.grid.component.GridPosition;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.Entity;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -25,7 +26,7 @@ final class GridMovementSystemTest {
subject.add(new GridControllable());
subject.add(new GridPosition(Vec2i.ZERO));
final Set<Entity> controllableEntities = Set.of(subject);
- when(world.query(Set.of(GridControllable.class, GridPosition.class))).thenReturn(controllableEntities);
+ when(world.resolve(Query.allOf(GridControllable.class, GridPosition.class))).thenReturn(controllableEntities);
final GridInputState inputState = GridInputState.builder().movement(Vec2i.SOUTH).build();
final GridMovementSystem system = new GridMovementSystem();
diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java
index c6a0cfe..b8b2d1c 100644
--- a/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java
+++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystemTest.java
@@ -10,6 +10,7 @@ import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.Entity;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -24,7 +25,7 @@ final class GridPhysicsSystemTest {
final Entity body = Entity.builder().id(3).build();
body.add(new GridPosition(Vec2i.ZERO));
body.add(new GridMomentum(Vec2i.EAST));
- when(world.query(Set.of(GridMomentum.class, GridPosition.class))).thenReturn(Set.of(body));
+ when(world.resolve(Query.allOf(GridMomentum.class, GridPosition.class))).thenReturn(Set.of(body));
final GridPhysicsSystem system = new GridPhysicsSystem();
system.update(world, GridInputState.builder().movement(Vec2i.NORTH).build(), Duration.ZERO);
diff --git a/core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java b/core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java
new file mode 100644
index 0000000..6829417
--- /dev/null
+++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java
@@ -0,0 +1,97 @@
+package coffee.liz.abstractionengine.grid.system;
+
+import coffee.liz.abstractionengine.grid.component.GridInputState;
+import coffee.liz.abstractionengine.grid.component.GridMomentum;
+import coffee.liz.abstractionengine.grid.component.GridPosition;
+import coffee.liz.abstractionengine.grid.component.LambdaTerm;
+import coffee.liz.abstractionengine.grid.component.Substitution;
+import coffee.liz.ecs.DAGWorld;
+import coffee.liz.ecs.math.Vec2;
+import coffee.liz.ecs.math.Vec2i;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
+import coffee.liz.lambda.ast.SourceCode;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class LambdaEvaluatorSystemTest {
+ private static final Vec2<Integer> GRID_DIMENSIONS = Vec2i.builder().x(3).y(3).build();
+ private static final SourceCode IDENTITY = SourceCode.ofArrow("(x -> x)(y)");
+ private static final SourceCode FREE = SourceCode.ofArrow("z");
+
+ @Test
+ public void testDependenciesSatisfied() {
+ final World<GridInputState> world = new DAGWorld<>(new GridIndexSystem(GRID_DIMENSIONS),
+ new LambdaEvaluatorSystem(SourceCode.Syntax.ARROW));
+
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(1).y(1).build())).add(new LambdaTerm(IDENTITY))
+ .add(new Substitution(Vec2i.EAST, Map.of("y", Vec2i.builder().x(0).y(0).build())));
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(0).y(0).build())).add(new LambdaTerm(FREE));
+
+ world.update(GridInputState.builder().build(), Duration.ZERO);
+
+ assertEquals(3, world.resolve(Query.allOf()).size());
+ final Set<Entity> emittedTerms = world.resolve(Query.allOf(GridMomentum.class, LambdaTerm.class));
+ assertEquals(1, emittedTerms.size());
+ final SourceCode emitted = emittedTerms.iterator().next().get(LambdaTerm.class).getTerm();
+ final SourceCode.Arrow arrow = assertInstanceOf(SourceCode.Arrow.class, emitted);
+ assertEquals("z", arrow.source());
+ }
+
+ @Test
+ public void testEmitsLambdaSyntax() {
+ final World<GridInputState> world = new DAGWorld<>(new GridIndexSystem(GRID_DIMENSIONS),
+ new LambdaEvaluatorSystem(SourceCode.Syntax.LAMBDA));
+
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(1).y(1).build())).add(new LambdaTerm(IDENTITY))
+ .add(new Substitution(Vec2i.EAST, Map.of("y", Vec2i.builder().x(0).y(0).build())));
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(0).y(0).build())).add(new LambdaTerm(FREE));
+
+ world.update(GridInputState.builder().build(), Duration.ZERO);
+
+ final Set<Entity> emittedTerms = world.resolve(Query.allOf(GridMomentum.class, LambdaTerm.class));
+ assertEquals(1, emittedTerms.size());
+ final SourceCode emitted = emittedTerms.iterator().next().get(LambdaTerm.class).getTerm();
+ final SourceCode.Lambda lambda = assertInstanceOf(SourceCode.Lambda.class, emitted);
+ assertEquals("z", lambda.source());
+ }
+
+ @Test
+ public void testDependenciesUnsatisfied() {
+ final World<GridInputState> world = new DAGWorld<>(new GridIndexSystem(GRID_DIMENSIONS),
+ new LambdaEvaluatorSystem(SourceCode.Syntax.ARROW));
+
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(1).y(1).build())).add(new LambdaTerm(IDENTITY))
+ .add(new Substitution(Vec2i.EAST, Map.of("y", Vec2i.builder().x(0).y(0).build())));
+
+ world.update(GridInputState.builder().build(), Duration.ZERO);
+
+ assertEquals(1, world.resolve(Query.allOf()).size());
+ assertEquals(0, world.resolve(Query.allOf(GridMomentum.class, LambdaTerm.class)).size());
+ }
+
+ @Test
+ public void testSpawnsInSubstitutionDirection() {
+ final Vec2<Integer> spawnMomentum = Vec2i.builder().x(0).y(-1).build();
+ final World<GridInputState> world = new DAGWorld<>(new GridIndexSystem(GRID_DIMENSIONS),
+ new LambdaEvaluatorSystem(SourceCode.Syntax.ARROW));
+
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(1).y(1).build())).add(new LambdaTerm(IDENTITY))
+ .add(new Substitution(spawnMomentum, Map.of("y", Vec2i.builder().x(0).y(0).build())));
+ world.createEntity().add(new GridPosition(Vec2i.builder().x(0).y(0).build())).add(new LambdaTerm(FREE));
+
+ world.update(GridInputState.builder().build(), Duration.ZERO);
+
+ final Set<Entity> emittedTerms = world.resolve(Query.allOf(GridMomentum.class, LambdaTerm.class));
+ assertEquals(1, emittedTerms.size());
+ final GridMomentum momentum = emittedTerms.iterator().next().get(GridMomentum.class);
+ assertEquals(new GridMomentum(spawnMomentum), momentum);
+ }
+}
diff --git a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
index 648745b..b596876 100644
--- a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
+++ b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
@@ -8,55 +8,60 @@ import coffee.liz.ecs.model.Component;
import coffee.liz.ecs.model.Entity;
import coffee.liz.ecs.model.System;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import java.time.Duration;
import java.util.Collection;
-import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
-final class DAGWorldTest {
- @Test
- public void queryReturnsEntitiesMatchingAllRequestedComponents() {
- final DAGWorld<String> world = new DAGWorld<>(Map.of());
- final Entity matching = world.createEntity();
- matching.add(new PositionComponent());
- matching.add(new VelocityComponent());
- final Entity partial = world.createEntity();
- partial.add(new PositionComponent());
- final Entity nonMatching = world.createEntity();
- nonMatching.add(new VelocityComponent());
+public class DAGWorldTest {
+ @ParameterizedTest
+ @MethodSource("queryScenarios")
+ public void queryResolvesExpectedEntities(final Query query, final Set<String> expectedEntityKeys) {
+ final World<String> world = new DAGWorld<>();
+ final Entity both = world.createEntity();
+ both.add(new PositionComponent());
+ both.add(new VelocityComponent());
+ final Entity positionOnly = world.createEntity();
+ positionOnly.add(new PositionComponent());
+ final Entity velocityOnly = world.createEntity();
+ velocityOnly.add(new VelocityComponent());
+ final Entity neither = world.createEntity();
world.update("state", Duration.ZERO);
- final Set<Entity> results = world.query(Set.of(PositionComponent.class, VelocityComponent.class));
-
- assertEquals(Set.of(matching), results);
+ final Map<String, Entity> entities = Map.of("both", both, "positionOnly", positionOnly, "velocityOnly",
+ velocityOnly, "neither", neither);
+ final Set<Entity> expectedEntities = expectedEntityKeys.stream().map(entities::get).collect(Collectors.toSet());
+ assertEquals(expectedEntities, world.resolve(query));
}
- @Test
- public void queryWithoutComponentsReturnsAllEntities() {
- final DAGWorld<String> world = new DAGWorld<>(Map.of());
- final Entity entityOne = world.createEntity();
- final Entity entityTwo = world.createEntity();
-
- final Set<Entity> results = world.query(List.<Class<? extends Component>>of());
-
- assertEquals(Set.of(entityOne, entityTwo), results);
+ private static Stream<Arguments> queryScenarios() {
+ return Stream.of(Arguments.of(Query.allOf(PositionComponent.class, VelocityComponent.class), Set.of("both")),
+ Arguments.of(Query.allOf(), Set.of("both", "positionOnly", "velocityOnly", "neither")),
+ Arguments.of(Query.anyOf(PositionComponent.class, VelocityComponent.class),
+ Set.of("both", "positionOnly", "velocityOnly")),
+ Arguments.of(Query.noneOf(PositionComponent.class, VelocityComponent.class), Set.of("neither")));
}
@Test
public void updateExecutesSystemsInTopologicalOrder() {
final CopyOnWriteArrayList<String> executionLog = new CopyOnWriteArrayList<>();
- final DAGWorld<String> world = new DAGWorld<>(Map.of(SystemC.class, new SystemC(executionLog), SystemA.class,
- new SystemA(executionLog), SystemB.class, new SystemB(executionLog)));
+ final DAGWorld<String> world = new DAGWorld<>(new SystemC(executionLog), new SystemA(executionLog),
+ new SystemB(executionLog));
world.update("state", Duration.ZERO);
assertEquals(List.of("A", "B", "C"), executionLog);
@@ -64,29 +69,27 @@ final class DAGWorldTest {
@Test
public void updateRefreshesComponentCacheAfterEntityMutations() {
- final DAGWorld<String> world = new DAGWorld<>(Map.of());
+ final DAGWorld<String> world = new DAGWorld<>();
final Entity subject = world.createEntity();
world.update("state", Duration.ZERO);
- assertTrue(world.query(Set.of(PositionComponent.class)).isEmpty());
+ assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty());
subject.add(new PositionComponent());
world.update("state", Duration.ZERO);
- assertEquals(1, world.query(Set.of(PositionComponent.class)).size());
+ assertEquals(1, world.resolve(Query.allOf(PositionComponent.class)).size());
subject.remove(PositionComponent.class);
world.update("state", Duration.ZERO);
- assertTrue(world.query(Set.of(PositionComponent.class)).isEmpty());
+ assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty());
}
@Test
public void queryFindsComponentsAddedByEarlierSystemInSameTick() {
final List<Set<Entity>> queryResults = new CopyOnWriteArrayList<>();
- final Map<Class<? extends System<String>>, System<String>> systems = new LinkedHashMap<>();
- systems.put(ComponentAdderSystem.class, new ComponentAdderSystem());
- systems.put(ComponentReaderSystem.class, new ComponentReaderSystem(queryResults));
- final DAGWorld<String> world = new DAGWorld<>(systems);
+ final DAGWorld<String> world = new DAGWorld<>(new ComponentAdderSystem(),
+ new ComponentReaderSystem(queryResults));
final Entity entity = world.createEntity();
entity.add(new PositionComponent());
@@ -99,13 +102,7 @@ final class DAGWorldTest {
@Test
public void circularDependencyDetectionThrowsIllegalStateException() {
- final Map<Class<? extends System<String>>, System<String>> systems = new LinkedHashMap<>();
- final SystemCycleA systemA = new SystemCycleA();
- final SystemCycleB systemB = new SystemCycleB();
- systems.put(SystemCycleA.class, systemA);
- systems.put(SystemCycleB.class, systemB);
-
- assertThrows(IllegalStateException.class, () -> new DAGWorld<>(systems));
+ assertThrows(IllegalStateException.class, () -> new DAGWorld<>(new SystemCycleA(), new SystemCycleB()));
}
private static final class PositionComponent implements Component {
@@ -188,7 +185,7 @@ final class DAGWorldTest {
@Override
public void update(final World<String> world, final String state, final Duration duration) {
- world.query(Set.of(PositionComponent.class)).forEach(e -> e.add(new VelocityComponent()));
+ world.resolve(Query.allOf(PositionComponent.class)).forEach(e -> e.add(new VelocityComponent()));
}
}
@@ -203,7 +200,7 @@ final class DAGWorldTest {
@Override
public void update(final World<String> world, final String state, final Duration duration) {
- queryResults.add(world.query(Set.of(VelocityComponent.class, PositionComponent.class)));
+ queryResults.add(world.resolve(Query.allOf(VelocityComponent.class, PositionComponent.class)));
}
}
}
diff --git a/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java b/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java
new file mode 100644
index 0000000..c1ab704
--- /dev/null
+++ b/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java
@@ -0,0 +1,60 @@
+package coffee.liz.lambda.format;
+
+import coffee.liz.lambda.LambdaDriver;
+import coffee.liz.lambda.ast.SourceCode;
+import coffee.liz.lambda.eval.Value;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ValueEmitterTest {
+ @Test
+ void emitsFreeInBothSyntaxes() {
+ final Value value = LambdaDriver.interpret(SourceCode.ofArrow("z"));
+
+ final SourceCode arrow = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
+ final SourceCode lambda = ValueEmitter.emit(value, SourceCode.Syntax.LAMBDA);
+
+ assertEquals("z", assertInstanceOf(SourceCode.Arrow.class, arrow).source());
+ assertEquals("z", assertInstanceOf(SourceCode.Lambda.class, lambda).source());
+ }
+
+ @Test
+ void emitsApplicationInBothSyntaxes() {
+ final Value value = LambdaDriver.interpret(SourceCode.ofArrow("f(x)"));
+
+ final SourceCode arrow = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
+ final SourceCode lambda = ValueEmitter.emit(value, SourceCode.Syntax.LAMBDA);
+
+ assertEquals("f(x)", assertInstanceOf(SourceCode.Arrow.class, arrow).source());
+ assertEquals("f x", assertInstanceOf(SourceCode.Lambda.class, lambda).source());
+ }
+
+ @Test
+ void emitsClosureWithCaptureMacros() {
+ final Value value = LambdaDriver.interpret(SourceCode.ofArrow("(x -> y -> x)(z)"));
+ final SourceCode emitted = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
+ final String source = assertInstanceOf(SourceCode.Arrow.class, emitted).source();
+
+ assertTrue(source.contains("let _cap"));
+ assertTrue(source.contains("y -> _cap"));
+
+ // Validate emitted program is parsable and evaluates.
+ final Value roundTrip = LambdaDriver.interpret(emitted);
+ assertInstanceOf(Value.Closure.class, roundTrip);
+ }
+
+ @Test
+ void emitsHygienicCapturesWhenNamesShadow() {
+ final Value value = LambdaDriver.interpret(SourceCode.ofArrow("(x -> y -> x)(y)"));
+ final SourceCode emitted = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
+ final String source = assertInstanceOf(SourceCode.Arrow.class, emitted).source();
+
+ assertTrue(source.contains("let _cap"));
+ assertTrue(source.contains("y -> _cap"));
+ assertFalse(source.contains("let y ="));
+ }
+}