aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2026-02-15 10:08:30 -0800
committerElizabeth Hunt <me@liz.coffee>2026-02-15 10:12:08 -0800
commitd6a294e14604aecd9046cbc19dc1837ca694fbb0 (patch)
tree616e87e59f9ee95253acce05aa26405523a9acd7 /core
parent347564c592325007cb81dbdf1a07831c2ae003c3 (diff)
downloadthe-abstraction-engine-java-d6a294e14604aecd9046cbc19dc1837ca694fbb0.tar.gz
the-abstraction-engine-java-d6a294e14604aecd9046cbc19dc1837ca694fbb0.zip
feat: Adding ValueEmitter, LambdaEvaluatorSystem
Diffstat (limited to 'core')
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/AbstractionEngineGridWorld.java11
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/actor/Abstraction.java18
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java5
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java7
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java5
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridWorldBridgeSystem.java18
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java28
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java6
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/navigation/TextNavigation.java6
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/entity/EntityFactory.java5
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/component/GridInputState.java15
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/component/LambdaTerm.java12
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java (renamed from core/src/main/java/coffee/liz/abstractionengine/component/LambdaTerm.java)8
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/system/BaseGridIndexSystem.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystem.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/system/GridMovementSystem.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystem.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java82
-rw-r--r--core/src/main/java/coffee/liz/ecs/DAGWorld.java85
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/Query.java30
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java25
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/System.java5
-rw-r--r--core/src/main/java/coffee/liz/ecs/model/World.java20
-rw-r--r--core/src/main/java/coffee/liz/lambda/LambdaDriver.java7
-rw-r--r--core/src/main/java/coffee/liz/lambda/eval/Environment.java28
-rw-r--r--core/src/main/java/coffee/liz/lambda/eval/NormalOrderEvaluator.java5
-rw-r--r--core/src/main/java/coffee/liz/lambda/format/Formatter.java10
-rw-r--r--core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java176
-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
41 files changed, 737 insertions, 177 deletions
diff --git a/core/src/main/java/coffee/liz/abstractionengine/AbstractionEngineGridWorld.java b/core/src/main/java/coffee/liz/abstractionengine/AbstractionEngineGridWorld.java
index 806d05e..b2ccb5e 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/AbstractionEngineGridWorld.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/AbstractionEngineGridWorld.java
@@ -5,22 +5,21 @@ import coffee.liz.abstractionengine.grid.system.GridCollisionPropagatationSystem
import coffee.liz.abstractionengine.grid.system.GridIndexSystem;
import coffee.liz.abstractionengine.grid.system.GridMovementSystem;
import coffee.liz.abstractionengine.grid.system.GridPhysicsSystem;
+import coffee.liz.abstractionengine.grid.system.LambdaEvaluatorSystem;
import coffee.liz.ecs.DAGWorld;
import coffee.liz.ecs.math.Vec2;
+import coffee.liz.lambda.ast.SourceCode;
import lombok.extern.log4j.Log4j2;
import java.time.Duration;
-import java.util.Map;
/** Grid world implementation for the abstraction engine. */
@Log4j2
public class AbstractionEngineGridWorld extends DAGWorld<GridInputState> {
- /** Initialize world with systems and player. */
- public AbstractionEngineGridWorld(final Vec2<Integer> gridDimensions) {
- super(Map.of(GridMovementSystem.class, new GridMovementSystem(), GridPhysicsSystem.class,
- new GridPhysicsSystem(), GridIndexSystem.class, new GridIndexSystem(gridDimensions),
- GridCollisionPropagatationSystem.class, new GridCollisionPropagatationSystem()));
+ public AbstractionEngineGridWorld(final Vec2<Integer> gridDimensions, final SourceCode.Syntax preferredSyntax) {
+ super(new GridMovementSystem(), new GridPhysicsSystem(), new GridIndexSystem(gridDimensions),
+ new GridCollisionPropagatationSystem(), new LambdaEvaluatorSystem(preferredSyntax));
}
/**
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/actor/Abstraction.java b/core/src/main/java/coffee/liz/abstractionengine/app/actor/Abstraction.java
index 3ea4701..86a573c 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/actor/Abstraction.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/actor/Abstraction.java
@@ -22,7 +22,7 @@ public class Abstraction extends Actor {
@Getter
private final Animation<TextureRegion> animation;
- private final Entity abstractionEntity;
+ // private final Entity abstractionEntity;
private boolean isActive;
private float stateTime = 0f;
@@ -33,24 +33,20 @@ public class Abstraction extends Actor {
public Abstraction() {
final Texture texture = new Texture(Gdx.files.internal("sprites/abstraction.png"));
final TextureRegion[][] tmp = TextureRegion.split(texture, texture.getWidth() / ABSTRACTION_FRAMES.getX(),
- texture.getHeight() / ABSTRACTION_FRAMES.getY());
- final TextureRegion[] anim = {
- tmp[0][0],
- tmp[1][0],
- tmp[2][0],
- tmp[3][0]
- };
+ texture.getHeight() / ABSTRACTION_FRAMES.getY());
+ final TextureRegion[] anim = {tmp[0][0], tmp[1][0], tmp[2][0], tmp[3][0]};
this.animation = new Animation<>(0.25f, anim);
addListener(new InputListener() {
@Override
- public void enter (final InputEvent event, final float x, final float y, final int pointer, @Null final Actor fromActor) {
+ public void enter(final InputEvent event, final float x, final float y, final int pointer,
+ @Null final Actor fromActor) {
log.info("Entered mouse {}", event);
}
-
@Override
- public boolean touchDown (final InputEvent event, final float x, final float y, final int pointer, final int button) {
+ public boolean touchDown(final InputEvent event, final float x, final float y, final int pointer,
+ final int button) {
log.info("TouchDown!!!");
return false;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java b/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java
index c4bb5e3..8f75d97 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/actor/LifeGridActor.java
@@ -9,6 +9,7 @@ import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.math.Vec2f;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
@@ -98,7 +99,7 @@ public class LifeGridActor extends Actor {
}
private void drawCells(final Vec2<Float> cellSize) {
- world.query(Set.of(GridPosition.class, CellState.class)).forEach(entity -> {
+ world.resolve(Query.allOf(GridPosition.class, CellState.class)).forEach(entity -> {
final CellState state = entity.get(CellState.class);
final Vec2<Integer> gridPos = entity.get(GridPosition.class).getPosition();
final Vec2<Float> drawPos = gridPos.floatValue().scale(cellSize).plus(parallaxOffset);
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java b/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java
index 7662700..b5ed2d1 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/config/Settings.java
@@ -1,5 +1,6 @@
package coffee.liz.abstractionengine.app.config;
+import coffee.liz.lambda.ast.SourceCode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@@ -14,6 +15,8 @@ public class Settings {
private boolean playMusic = true;
@Builder.Default
private boolean vimMode = true;
+ @Builder.Default
+ private SourceCode.Syntax preferredSyntax = SourceCode.Syntax.ARROW;
@Builder.Default
private KeyBinds keyBinds = KeyBinds.builder().build();
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java
index 5660ff3..1ea5d32 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/life/EntropyAudioSystem.java
@@ -150,7 +150,7 @@ public class EntropyAudioSystem implements System<LifeInput> {
}
@Override
- public void close() {
+ public void dispose() {
running = false;
FunctionUtils.wrapCheckedException(() -> audioThread.join(AUDIO_THREAD_TIMEOUT));
device.dispose();
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
index 70bb973..e96127c 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/GameScreen.java
@@ -65,6 +65,6 @@ public class GameScreen implements Screen {
@Override
public void dispose() {
- FunctionUtils.wrapCheckedException(outerWorld::close);
+ FunctionUtils.wrapCheckedException(outerWorld::dispose);
}
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java
index 46b5192..b8cfbbc 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/MainMenu.java
@@ -34,8 +34,7 @@ public class MainMenu implements Screen {
private static final Vec2<Float> BUTTON_SIZE = AbstractionEngineGame.WORLD_SIZE.scale(1 / 4f, 1 / 15f);
private static final float PARALLAX_STRENGTH = 3f;
- private final World<LifeInput> world = new DAGWorld<>(Map.of(LifeSystem.class, new LifeSystem(GRID_DIMENSIONS),
- EntropyAudioSystem.class, new EntropyAudioSystem()));
+ private final World<LifeInput> world = new DAGWorld<>(new LifeSystem(GRID_DIMENSIONS), new EntropyAudioSystem());
private final AbstractionEngineGame game;
private final Set<Vec2<Integer>> initialAlive = createGliderPattern();
@@ -162,7 +161,7 @@ public class MainMenu implements Screen {
*/
@Override
public void dispose() {
- FunctionUtils.wrapCheckedException(world::close);
+ FunctionUtils.wrapCheckedException(world::dispose);
stage.dispose();
}
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java
index 137a5b0..260c191 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/GameScreenWorld.java
@@ -9,12 +9,9 @@ import coffee.liz.ecs.DAGWorld;
import coffee.liz.ecs.math.Vec2;
import com.badlogic.gdx.utils.viewport.FitViewport;
-import java.util.Map;
-
public class GameScreenWorld extends DAGWorld<FrameState> {
public GameScreenWorld(final Vec2<Integer> gridSize, final FitViewport viewport, final Settings settings) {
- super(Map.of(InputSystem.class, new InputSystem(), GridWorldBridgeSystem.class,
- new GridWorldBridgeSystem(gridSize), GridInterpolationSystem.class, new GridInterpolationSystem(),
- RenderSystem.class, new RenderSystem(viewport, gridSize, settings)));
+ super(new InputSystem(), new GridWorldBridgeSystem(gridSize, settings), new GridInterpolationSystem(),
+ new RenderSystem(viewport, gridSize, settings));
}
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java
index 0d2d34b..d0acfbc 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java
@@ -10,6 +10,7 @@ import coffee.liz.ecs.math.Vec2f;
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 java.time.Duration;
import java.util.Collection;
@@ -33,7 +34,7 @@ public class GridInterpolationSystem implements System<FrameState> {
public void update(final World<FrameState> world, final FrameState state, final Duration dt) {
final GridWorldBridgeSystem bridgeSystem = world.getSystem(GridWorldBridgeSystem.class);
- world.query(Set.of(ExternalWorldEntityRef.class, ActorHolder.class)).forEach(entity -> {
+ world.resolve(Query.allOf(ExternalWorldEntityRef.class, ActorHolder.class)).forEach(entity -> {
final Actor actor = entity.get(ActorHolder.class).getActor();
final boolean isAlreadyInterpolating = !actor.getActions().isEmpty();
if (isAlreadyInterpolating) {
@@ -62,7 +63,7 @@ public class GridInterpolationSystem implements System<FrameState> {
}
private Interpolation interpolationType(final EntityType type) {
- return switch(type) {
+ return switch (type) {
case PLAYER -> Interpolation.pow3;
default -> new Interpolation.Swing(1.15f);
};
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridWorldBridgeSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridWorldBridgeSystem.java
index dac5a32..3d76853 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridWorldBridgeSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridWorldBridgeSystem.java
@@ -5,6 +5,7 @@ import coffee.liz.abstractionengine.app.AbstractionEngineGame;
import coffee.liz.abstractionengine.app.actor.Abstraction;
import coffee.liz.abstractionengine.app.actor.Penguin;
import coffee.liz.abstractionengine.app.actor.Wall;
+import coffee.liz.abstractionengine.app.config.Settings;
import coffee.liz.abstractionengine.app.screen.game.FrameState;
import coffee.liz.abstractionengine.app.screen.game.component.ActorHolder;
import coffee.liz.abstractionengine.app.screen.game.component.ExternalWorldEntityRef;
@@ -18,6 +19,7 @@ import coffee.liz.ecs.math.Vec2i;
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.Getter;
import java.time.Duration;
@@ -41,12 +43,14 @@ public class GridWorldBridgeSystem implements System<FrameState> {
@Getter
private final Vec2<Float> worldToGrid;
- public GridWorldBridgeSystem(final Vec2<Integer> gridSize) {
+ public GridWorldBridgeSystem(final Vec2<Integer> gridSize, final Settings settings) {
this.gridSize = gridSize;
- this.outerWorld = new AbstractionEngineGridWorld(gridSize);
+ this.outerWorld = new AbstractionEngineGridWorld(gridSize, settings.getPreferredSyntax());
- this.gridToWorld = AbstractionEngineGame.WORLD_SIZE.scale(gridSize.floatValue().transform(x -> 1f / x, y -> 1f / y));
- this.worldToGrid = gridSize.floatValue().scale(AbstractionEngineGame.WORLD_SIZE.floatValue().transform(x -> 1f/x, y -> 1f/y));
+ this.gridToWorld = AbstractionEngineGame.WORLD_SIZE
+ .scale(gridSize.floatValue().transform(x -> 1f / x, y -> 1f / y));
+ this.worldToGrid = gridSize.floatValue()
+ .scale(AbstractionEngineGame.WORLD_SIZE.floatValue().transform(x -> 1f / x, y -> 1f / y));
}
public void initLevel() {
@@ -71,7 +75,7 @@ public class GridWorldBridgeSystem implements System<FrameState> {
}
private void syncEntities(final World<FrameState> world) {
- final Set<Entity> gridEntities = outerWorld.query(Set.of(GridPosition.class, EntityType.class));
+ final Set<Entity> gridEntities = outerWorld.resolve(Query.allOf(GridPosition.class, EntityType.class));
final Set<Integer> seenGridIds = new HashSet<>();
for (final Entity gridEntity : gridEntities) {
@@ -100,8 +104,8 @@ public class GridWorldBridgeSystem implements System<FrameState> {
}
@Override
- public void close() {
- FunctionUtils.wrapCheckedException(outerWorld::close);
+ public void dispose() {
+ FunctionUtils.wrapCheckedException(outerWorld::dispose);
}
private Actor createActor(final EntityType type, final Stage stage) {
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java
index 401ed89..32b9ebd 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java
@@ -70,7 +70,7 @@ public class RenderSystem implements System<FrameState> {
}
@Override
- public void close() {
+ public void dispose() {
uiStage.dispose();
stage.dispose();
shapeRenderer.dispose();
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java
index abae5fb..285bbde 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java
@@ -19,6 +19,9 @@ import coffee.liz.abstractionengine.app.ui.editor.vim.state.PendingOperator;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualLineState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualState;
import com.badlogic.gdx.Input;
+import lombok.Getter;
+import lombok.Setter;
+
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Consumer;
@@ -31,6 +34,7 @@ public class KeyContext {
private final Register register;
private final VimConfig config;
+ @Getter
private String statusText;
private String lastSearchQuery;
private SearchDirection lastSearchDirection;
@@ -39,6 +43,7 @@ public class KeyContext {
private String searchIteratorText;
private int suppressedTypedCommands;
private boolean suppressNextRedoTyped;
+ @Setter
private KeyEvent currentKeyEvent;
public KeyContext(final EditorBuffer editorBuffer, final Consumer<String> statusUpdater,
@@ -73,18 +78,10 @@ public class KeyContext {
return config;
}
- public void setCurrentKeyEvent(final KeyEvent currentKeyEvent) {
- this.currentKeyEvent = currentKeyEvent;
- }
-
public KeyEvent currentKeyEvent() {
return currentKeyEvent;
}
- public String getStatusText() {
- return statusText;
- }
-
public void publishStatus(final String status) {
if (!status.equals(statusText)) {
statusText = status;
@@ -361,7 +358,7 @@ public class KeyContext {
public void deleteCharUnderCursor() {
final String text = editorBuffer.getText();
final int cursor = editorBuffer.getCursorPosition();
- if (cursor >= text.length() || text.charAt(cursor) == '\n') {
+ if (cursor >= text.length() || text.charAt(cursor) == ' ') {
return;
}
register.setText(String.valueOf(text.charAt(cursor)));
@@ -432,7 +429,9 @@ public class KeyContext {
return;
}
- final int insertAt = Math.min(cursor + 1, text.length());
+ final boolean cursorOnEmptyLine = cursor < text.length() && text.charAt(cursor) == '\n'
+ && navigation.findLineStart(text, cursor) == cursor;
+ final int insertAt = cursorOnEmptyLine ? cursor : Math.min(cursor + 1, text.length());
editorBuffer.setText(text.substring(0, insertAt) + register.getText() + text.substring(insertAt));
editorBuffer.setCursorPosition(insertAt + register.getText().length() - 1);
invalidateSearchIterator();
@@ -452,9 +451,8 @@ public class KeyContext {
return;
}
- final int insertAt = cursor;
- editorBuffer.setText(text.substring(0, insertAt) + register.getText() + text.substring(insertAt));
- editorBuffer.setCursorPosition(insertAt + register.getText().length() - 1);
+ editorBuffer.setText(text.substring(0, cursor) + register.getText() + text.substring(cursor));
+ editorBuffer.setCursorPosition(cursor + register.getText().length() - 1);
invalidateSearchIterator();
}
@@ -737,10 +735,6 @@ public class KeyContext {
editorBuffer.clearSelection();
}
- public void setSelectionEndpoints(final int selectionStart, final int cursorPosition) {
- editorBuffer.setSelectionEndpoints(selectionStart, cursorPosition);
- }
-
public Register register() {
return register;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java
index bd9b1d0..95dc2bc 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java
@@ -84,8 +84,8 @@ public class NormalModeHandler implements ModeHandler<NormalState> {
if (character == 'i' && (operator == PendingOperator.DELETE || operator == PendingOperator.YANK
|| operator == PendingOperator.CHANGE)) {
- return HandleResult.handled(new NormalState(state.count(), mapInnerOperator(operator),
- state.preferredColumn()));
+ return HandleResult
+ .handled(new NormalState(state.count(), mapInnerOperator(operator), state.preferredColumn()));
}
if (isInnerWordOperator(operator)) {
@@ -96,7 +96,7 @@ public class NormalModeHandler implements ModeHandler<NormalState> {
}
final int start = context.cursor();
- final int updatedPreferred = applyMotion(character, repeats, state.preferredColumn(), context, true);
+ final int updatedPreferred = applyMotion(character, repeats, state.preferredColumn(), context, false);
int end = context.cursor();
if (character == '$') {
end = Math.min(context.text().length(), end + 1);
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/navigation/TextNavigation.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/navigation/TextNavigation.java
index b74c1df..10af095 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/navigation/TextNavigation.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/navigation/TextNavigation.java
@@ -85,7 +85,11 @@ public class TextNavigation {
return 0;
}
- return text.length() - 1;
+ int index = text.length() - 1;
+ while (index > 0 && (text.charAt(index) == '\n' || text.charAt(index) == '\r')) {
+ index--;
+ }
+ return index;
}
public int maxCursorPositionInNormalMode(final String text) {
diff --git a/core/src/main/java/coffee/liz/abstractionengine/entity/EntityFactory.java b/core/src/main/java/coffee/liz/abstractionengine/entity/EntityFactory.java
index d8299aa..871c6d0 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/entity/EntityFactory.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/entity/EntityFactory.java
@@ -2,6 +2,7 @@ package coffee.liz.abstractionengine.entity;
import coffee.liz.abstractionengine.grid.component.GridControllable;
import coffee.liz.abstractionengine.grid.component.GridPosition;
+import coffee.liz.abstractionengine.grid.component.LambdaTerm;
import coffee.liz.ecs.math.Mat2;
import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.model.Entity;
@@ -51,6 +52,10 @@ public final class EntityFactory {
return world.createEntity().add(new GridPosition(position)).add(EntityType.ABSTRACTION)
.add(new EntityTypeGridCollidable());
}
+
+ public static Entity addToWorld(final World<?> world, final Vec2<Integer> position, final LambdaTerm term) {
+ return addToWorld(world, position).add(term);
+ }
}
/**
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/component/GridInputState.java b/core/src/main/java/coffee/liz/abstractionengine/grid/component/GridInputState.java
index 6ab0d2b..5134719 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/component/GridInputState.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/component/GridInputState.java
@@ -19,19 +19,4 @@ public class GridInputState {
/** Movement {@link Vec2} direction. */
@Nullable
private final Vec2<Integer> movement;
-
- /** Lambda term update to apply. */
- @Nullable
- private final UpdateLambdaTerm updateLambdaTerm;
-
- /** Update lambda term with target entity. */
- @Getter
- @RequiredArgsConstructor
- public static class UpdateLambdaTerm {
- /** {@link Entity} to update. */
- private final Entity toUpdate;
-
- /** {@link LambdaProgram} to apply. */
- private final LambdaProgram lambdaProgram;
- }
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/component/LambdaTerm.java b/core/src/main/java/coffee/liz/abstractionengine/grid/component/LambdaTerm.java
new file mode 100644
index 0000000..9ed1e9c
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/component/LambdaTerm.java
@@ -0,0 +1,12 @@
+package coffee.liz.abstractionengine.grid.component;
+
+import coffee.liz.ecs.model.Component;
+import coffee.liz.lambda.ast.SourceCode;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+@Data
+public class LambdaTerm implements Component {
+ private final SourceCode term;
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/component/LambdaTerm.java b/core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java
index b0d1358..77ff4e8 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/component/LambdaTerm.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java
@@ -1,8 +1,7 @@
-package coffee.liz.abstractionengine.component;
+package coffee.liz.abstractionengine.grid.component;
import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.model.Component;
-import coffee.liz.lambda.ast.Expression;
import lombok.Data;
import lombok.RequiredArgsConstructor;
@@ -10,8 +9,7 @@ import java.util.Map;
@RequiredArgsConstructor
@Data
-public class LambdaTerm implements Component {
+public class Substitution implements Component {
+ private final Vec2<Integer> spawnMomentum;
private final Map<String, Vec2<Integer>> termDependencies;
-
- private final Expression term;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/system/BaseGridIndexSystem.java b/core/src/main/java/coffee/liz/abstractionengine/grid/system/BaseGridIndexSystem.java
index ee77256..470c72f 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/system/BaseGridIndexSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/system/BaseGridIndexSystem.java
@@ -4,6 +4,7 @@ import coffee.liz.abstractionengine.grid.component.GridPosition;
import coffee.liz.ecs.math.Mat2;
import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.Query;
import coffee.liz.ecs.model.System;
import coffee.liz.ecs.model.World;
import lombok.Getter;
@@ -36,7 +37,7 @@ public abstract class BaseGridIndexSystem<T> implements System<T> {
@Override
public void update(final World<T> world, final T state, final Duration dt) {
rows.forEach(row -> row.forEach(Set::clear));
- world.query(Set.of(GridPosition.class)).forEach(entity -> {
+ world.resolve(Query.allOf(GridPosition.class)).forEach(entity -> {
final GridPosition position = entity.get(GridPosition.class);
rows.get(position.getPosition().getY()).get(position.getPosition().getX()).add(entity);
});
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystem.java b/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystem.java
index 23a821d..9980795 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridCollisionPropagatationSystem.java
@@ -10,6 +10,7 @@ import coffee.liz.ecs.math.Vec2i;
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 java.time.Duration;
import java.util.ArrayList;
@@ -31,7 +32,7 @@ public class GridCollisionPropagatationSystem implements System<GridInputState>
@Override
public void update(final World<GridInputState> world, final GridInputState state, final Duration dt) {
- world.query(Set.of(GridMomentum.class, GridPosition.class, GridCollidable.class)).forEach(pusher -> {
+ world.resolve(Query.allOf(GridMomentum.class, GridPosition.class, GridCollidable.class)).forEach(pusher -> {
final Vec2<Integer> velocity = pusher.get(GridMomentum.class).getVelocity();
if (velocity.equals(Vec2i.ZERO)) {
return;
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridMovementSystem.java b/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridMovementSystem.java
index 02f012b..c402fcc 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridMovementSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridMovementSystem.java
@@ -6,6 +6,7 @@ import coffee.liz.abstractionengine.grid.component.GridMomentum;
import coffee.liz.abstractionengine.grid.component.GridPosition;
import coffee.liz.ecs.model.System;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -25,7 +26,7 @@ public class GridMovementSystem implements System<GridInputState> {
@Override
public void update(final World<GridInputState> world, final GridInputState state, final Duration dt) {
- world.query(Set.of(GridControllable.class, GridPosition.class))
+ world.resolve(Query.allOf(GridControllable.class, GridPosition.class))
.forEach(entity -> entity.add(new GridMomentum(state.getMovement())));
}
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystem.java b/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystem.java
index 5e0e632..676dd1b 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/system/GridPhysicsSystem.java
@@ -6,6 +6,7 @@ import coffee.liz.abstractionengine.grid.component.GridPosition;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.System;
import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
import java.time.Duration;
import java.util.Collection;
@@ -20,7 +21,7 @@ public class GridPhysicsSystem implements System<GridInputState> {
@Override
public void update(final World<GridInputState> world, final GridInputState state, final Duration dt) {
- world.query(Set.of(GridMomentum.class, GridPosition.class)).forEach(entity -> {
+ world.resolve(Query.allOf(GridMomentum.class, GridPosition.class)).forEach(entity -> {
final GridMomentum momentum = entity.get(GridMomentum.class);
final GridPosition position = entity.get(GridPosition.class);
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java b/core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java
new file mode 100644
index 0000000..74bc3fc
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java
@@ -0,0 +1,82 @@
+package coffee.liz.abstractionengine.grid.system;
+
+import coffee.liz.abstractionengine.entity.EntityFactory;
+import coffee.liz.abstractionengine.entity.EntityType;
+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.math.Vec2;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+import coffee.liz.ecs.model.Query;
+import coffee.liz.lambda.LambdaDriver;
+import coffee.liz.lambda.ast.SourceCode;
+import coffee.liz.lambda.eval.Environment;
+import coffee.liz.lambda.eval.Value;
+import coffee.liz.lambda.format.ValueEmitter;
+import lombok.extern.log4j.Log4j2;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+@Log4j2
+public class LambdaEvaluatorSystem implements System<GridInputState> {
+ private final SourceCode.Syntax preferredSyntax;
+
+ public LambdaEvaluatorSystem(final SourceCode.Syntax preferredSyntax) {
+ this.preferredSyntax = preferredSyntax;
+ }
+
+ @Override
+ public Collection<Class<? extends System<GridInputState>>> getDependencies() {
+ return List.of(GridCollisionPropagatationSystem.class);
+ }
+
+ @Override
+ public void update(final World<GridInputState> world, final GridInputState state, final Duration dt) {
+ final GridIndexSystem gridIndexSystem = world.getSystem(GridIndexSystem.class);
+
+ world.resolve(Query.allOf(Substitution.class, LambdaTerm.class, GridPosition.class)).forEach(entity -> {
+ final LambdaTerm term = entity.get(LambdaTerm.class);
+ final Substitution substitution = entity.get(Substitution.class);
+ final GridPosition position = entity.get(GridPosition.class);
+
+ final Map<String, Supplier<Value>> dependencies = substitution.getTermDependencies().entrySet().stream()
+ .map(e -> {
+ final String symbol = e.getKey();
+ // Ideally we should only ever have one match to replace the symbol with.
+ // But maybe there's a use-case where this is not the case?
+ final Optional<SourceCode> sourceCode = gridIndexSystem.entitiesAt(e.getValue()).stream()
+ .filter(_entity -> _entity.has(LambdaTerm.class))
+ .map(_entity -> _entity.get(LambdaTerm.class)).map(LambdaTerm::getTerm).findFirst();
+ // TODO: Map error
+ final Optional<Supplier<Value>> val = sourceCode.map(s -> () -> LambdaDriver.interpret(s));
+ return Map.entry(symbol, val);
+ }).filter(e -> e.getValue().isPresent())
+ .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get()));
+
+ final boolean dependenciesSatisified = dependencies.keySet()
+ .equals(substitution.getTermDependencies().keySet());
+ if (!dependenciesSatisified) {
+ return;
+ }
+
+ final Environment env = dependencies.entrySet().stream().reduce(Environment.root(),
+ (_env, e) -> _env.extend(e.getKey(), e.getValue()), (a, b) -> b);
+ final Value evaluationResult = LambdaDriver.interpret(term.getTerm(), List.of(), env);
+ final SourceCode emittedResult = ValueEmitter.emit(evaluationResult, preferredSyntax);
+ final Vec2<Integer> spawnMomentum = substitution.getSpawnMomentum();
+
+ EntityFactory.Abstraction.addToWorld(world, position.getPosition(), new LambdaTerm(emittedResult))
+ .add(EntityType.ABSTRACTION).add(new GridPosition(position.getPosition()))
+ .add(new GridMomentum(spawnMomentum));
+ });
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/DAGWorld.java b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
index 24d5adc..5d052ab 100644
--- a/core/src/main/java/coffee/liz/ecs/DAGWorld.java
+++ b/core/src/main/java/coffee/liz/ecs/DAGWorld.java
@@ -2,6 +2,7 @@ package coffee.liz.ecs;
import coffee.liz.ecs.model.Component;
import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.Query;
import coffee.liz.ecs.model.System;
import coffee.liz.ecs.model.World;
@@ -10,10 +11,12 @@ import lombok.extern.log4j.Log4j2;
import java.time.Duration;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@@ -42,9 +45,10 @@ public class DAGWorld<T> implements World<T> {
/** Ordered list of systems for execution. */
private final List<System<T>> systemExecutionOrder;
- public DAGWorld(final Map<Class<? extends System<T>>, System<T>> systems) {
- this.systems = systems;
- this.systemExecutionOrder = buildExecutionOrder(systems.values().stream().toList());
+ @SafeVarargs
+ public DAGWorld(final System<T>... systems) {
+ this.systems = singletonClazzMap(systems);
+ this.systemExecutionOrder = buildExecutionOrder(Arrays.asList(systems));
log.debug("Executing in order: {}", systemExecutionOrder);
}
@@ -67,19 +71,13 @@ public class DAGWorld<T> implements World<T> {
}
@Override
- public Set<Entity> query(final Collection<Class<? extends Component>> components) {
- if (components.isEmpty()) {
- return Set.copyOf(entities);
- }
-
- final Class<? extends Component> firstType = components.iterator().next();
- final Set<Entity> candidates = componentCache.get(firstType);
- if (candidates == null) {
- return Collections.emptySet();
- }
-
- return candidates.stream().filter(entity -> components.stream().allMatch(entity::has))
- .collect(Collectors.toSet());
+ public Set<Entity> resolve(final Query query) {
+ final Set<Class<? extends Component>> components = query.queryingComponents();
+ return switch (query.filter()) {
+ case ALL_OF -> resolveAllOf(components);
+ case ANY_OF -> resolveAnyOf(components);
+ case NONE_OF -> resolveNoneOf(components);
+ };
}
@Override
@@ -103,15 +101,48 @@ public class DAGWorld<T> implements World<T> {
componentType -> componentCache.computeIfAbsent(componentType, _ -> new HashSet<>()).add(entity)));
}
+ private Set<Entity> resolveAllOf(final Set<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Set.copyOf(entities);
+ }
+
+ final Class<? extends Component> firstType = components.iterator().next();
+ final Set<Entity> candidates = componentCache.get(firstType);
+ if (candidates == null) {
+ return Collections.emptySet();
+ }
+
+ return candidates.stream().filter(entity -> components.stream().allMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
+ private Set<Entity> resolveAnyOf(final Set<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ return entities.stream().filter(entity -> components.stream().anyMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
+ private Set<Entity> resolveNoneOf(final Set<Class<? extends Component>> components) {
+ if (components.isEmpty()) {
+ return Set.copyOf(entities);
+ }
+
+ return entities.stream().filter(entity -> components.stream().noneMatch(entity::has))
+ .collect(Collectors.toSet());
+ }
+
private List<System<T>> buildExecutionOrder(final Collection<System<T>> systems) {
if (systems.isEmpty()) {
return Collections.emptyList();
}
final Map<Class<?>, System<T>> systemMap = systems.stream()
- .collect(Collectors.toMap(System::getClass, system -> system));
- final Map<Class<?>, Integer> inDegree = new HashMap<>();
- final Map<Class<?>, Set<Class<?>>> adjacencyList = new HashMap<>();
+ .collect(Collectors.toMap(System::getClass, system -> system, (_, b) -> b, LinkedHashMap::new));
+ final Map<Class<?>, Integer> inDegree = new LinkedHashMap<>();
+ final Map<Class<?>, Set<Class<?>>> adjacencyList = new LinkedHashMap<>();
systems.forEach(system -> {
final Class<?> systemClass = system.getClass();
@@ -155,11 +186,23 @@ public class DAGWorld<T> implements World<T> {
}
@Override
- public void close() throws Exception {
+ public void dispose() {
for (final System<T> system : systemExecutionOrder) {
- system.close();
+ system.dispose();
}
componentCache.clear();
entities.clear();
}
+
+ @SuppressWarnings("unchecked")
+ private static <T> Map<Class<? extends T>, T> singletonClazzMap(final T... singletons) {
+ final boolean areSingletons = Arrays.stream(singletons).map(t -> (Class<? extends System<T>>) t.getClass())
+ .distinct().count() == singletons.length;
+ if (!areSingletons) {
+ throw new IllegalArgumentException("Only one instance may be used per clazz");
+ }
+
+ return Arrays.stream(singletons).map(t -> Map.entry((Class<? extends T>) t.getClass(), t))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ }
}
diff --git a/core/src/main/java/coffee/liz/ecs/model/Query.java b/core/src/main/java/coffee/liz/ecs/model/Query.java
new file mode 100644
index 0000000..679ea49
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/Query.java
@@ -0,0 +1,30 @@
+package coffee.liz.ecs.model;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public record Query(Set<Class<? extends Component>> queryingComponents, QueryFilter filter) {
+ public Query {
+ queryingComponents = Set.copyOf(queryingComponents);
+ }
+
+ @SafeVarargs
+ public static Query allOf(final Class<? extends Component>... components) {
+ return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ALL_OF);
+ }
+
+ @SafeVarargs
+ public static Query anyOf(final Class<? extends Component>... components) {
+ return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.ANY_OF);
+ }
+
+ @SafeVarargs
+ public static Query noneOf(final Class<? extends Component>... components) {
+ return new Query(Arrays.stream(components).collect(Collectors.toSet()), QueryFilter.NONE_OF);
+ }
+
+ public enum QueryFilter {
+ ALL_OF, ANY_OF, NONE_OF
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java
new file mode 100644
index 0000000..eba5021
--- /dev/null
+++ b/core/src/main/java/coffee/liz/ecs/model/QueryBuilder.java
@@ -0,0 +1,25 @@
+package coffee.liz.ecs.model;
+
+import lombok.RequiredArgsConstructor;
+
+import java.util.Set;
+
+@RequiredArgsConstructor
+public class QueryBuilder<T> {
+ private final World<T> world;
+
+ @SafeVarargs
+ public final Set<Entity> allOf(final Class<? extends Component>... components) {
+ return world.resolve(Query.allOf(components));
+ }
+
+ @SafeVarargs
+ public final Set<Entity> anyOf(final Class<? extends Component>... components) {
+ return world.resolve(Query.anyOf(components));
+ }
+
+ @SafeVarargs
+ public final Set<Entity> noneOf(final Class<? extends Component>... components) {
+ return world.resolve(Query.noneOf(components));
+ }
+}
diff --git a/core/src/main/java/coffee/liz/ecs/model/System.java b/core/src/main/java/coffee/liz/ecs/model/System.java
index c4b0bc0..8138a0b 100644
--- a/core/src/main/java/coffee/liz/ecs/model/System.java
+++ b/core/src/main/java/coffee/liz/ecs/model/System.java
@@ -9,7 +9,7 @@ import java.util.Collection;
* @param <T>
* is the state of the stuff outside the {@link World}.
*/
-public interface System<T> extends AutoCloseable {
+public interface System<T> {
/**
* {@link System} clazzes that must run before this system.
*
@@ -27,7 +27,6 @@ public interface System<T> extends AutoCloseable {
*/
void update(final World<T> world, final T state, final Duration dt);
- @Override
- default void close() {
+ default void dispose() {
}
}
diff --git a/core/src/main/java/coffee/liz/ecs/model/World.java b/core/src/main/java/coffee/liz/ecs/model/World.java
index c59a0d7..668c3d5 100644
--- a/core/src/main/java/coffee/liz/ecs/model/World.java
+++ b/core/src/main/java/coffee/liz/ecs/model/World.java
@@ -1,16 +1,15 @@
package coffee.liz.ecs.model;
import java.time.Duration;
-import java.util.Collection;
import java.util.Set;
/**
- * The state of the world.
+ * The game world.
*
* @param <T>
* is the state of the stuff outside the world.
*/
-public interface World<T> extends AutoCloseable {
+public interface World<T> {
/**
* Create unique {@link Entity} in the {@link World}.
*
@@ -29,11 +28,15 @@ public interface World<T> extends AutoCloseable {
/**
* Get entities with {@link Component}s.
*
- * @param components
- * to query for.
- * @return All entities with all {@param components}.
+ * @param query
+ * to query.
+ * @return All entities satisfying {@param query}.
*/
- Set<Entity> query(final Collection<Class<? extends Component>> components);
+ Set<Entity> resolve(final Query query);
+
+ default QueryBuilder<T> queryable() {
+ return new QueryBuilder<>(this);
+ }
/**
* Integrate the {@link World}.
@@ -55,4 +58,7 @@ public interface World<T> extends AutoCloseable {
* @return {@link System} instance of {@param system}.
*/
<S extends System<T>> S getSystem(final Class<S> system);
+
+ default void dispose() {
+ }
}
diff --git a/core/src/main/java/coffee/liz/lambda/LambdaDriver.java b/core/src/main/java/coffee/liz/lambda/LambdaDriver.java
index 59cee25..475e14a 100644
--- a/core/src/main/java/coffee/liz/lambda/LambdaDriver.java
+++ b/core/src/main/java/coffee/liz/lambda/LambdaDriver.java
@@ -71,9 +71,14 @@ public class LambdaDriver {
* @return the evaluated result
*/
public static Value interpret(final SourceCode sourceCode, final List<ExternalBinding> bindings) {
+ return interpret(sourceCode, bindings, Environment.root());
+ }
+
+ public static Value interpret(final SourceCode sourceCode, final List<ExternalBinding> bindings,
+ final Environment baseEnvironment) {
final LambdaProgram program = parse(sourceCode);
final Expression expression = program.expression();
final List<Macro> macros = program.macros();
- return NormalOrderEvaluator.evaluate(expression, Environment.from(macros, bindings));
+ return NormalOrderEvaluator.evaluate(expression, baseEnvironment.extend(Environment.from(macros, bindings)));
}
}
diff --git a/core/src/main/java/coffee/liz/lambda/eval/Environment.java b/core/src/main/java/coffee/liz/lambda/eval/Environment.java
index a4ec24a..5178385 100644
--- a/core/src/main/java/coffee/liz/lambda/eval/Environment.java
+++ b/core/src/main/java/coffee/liz/lambda/eval/Environment.java
@@ -4,9 +4,11 @@ import coffee.liz.lambda.ast.Expression;
import coffee.liz.lambda.ast.Macro;
import coffee.liz.lambda.bind.ExternalBinding;
import jakarta.annotation.Nullable;
+import lombok.Builder;
import lombok.RequiredArgsConstructor;
import java.util.List;
+import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
@@ -17,6 +19,7 @@ import java.util.stream.Collectors;
* Runtime environment for variable bindings, macros, and external bindings.
*/
@RequiredArgsConstructor
+@Builder(toBuilder = true)
public final class Environment {
/** Named expansions */
private final Map<String, Expression> macros;
@@ -64,6 +67,10 @@ public final class Environment {
return new Environment(macros, externalBindings, name, value, this);
}
+ public Environment extend(final Environment other) {
+ return other.toBuilder().parent(this).build();
+ }
+
/**
* Looks up a name, checking bindings, then macros, then external bindings.
*
@@ -93,6 +100,20 @@ public final class Environment {
}
/**
+ * Returns local binding frames from nearest scope to farthest.
+ */
+ public List<BindingFrame> bindingFrames() {
+ final List<BindingFrame> frames = new ArrayList<>();
+ for (Environment env = this; env != null; env = env.parent) {
+ if (env.boundName == null || env.boundValue == null) {
+ continue;
+ }
+ frames.add(new BindingFrame(env.boundName, env.boundValue));
+ }
+ return List.copyOf(frames);
+ }
+
+ /**
* Result of looking up a name in the environment.
*/
public sealed interface LookupResult {
@@ -106,4 +127,11 @@ public final class Environment {
record External(ExternalBinding binding) implements LookupResult {
}
}
+
+ public record BindingFrame(String name, Supplier<Value> value) {
+ }
+
+ public static Environment root() {
+ return new Environment(Map.of(), Map.of(), null, null, null);
+ }
}
diff --git a/core/src/main/java/coffee/liz/lambda/eval/NormalOrderEvaluator.java b/core/src/main/java/coffee/liz/lambda/eval/NormalOrderEvaluator.java
index d9bcba3..4587f4b 100644
--- a/core/src/main/java/coffee/liz/lambda/eval/NormalOrderEvaluator.java
+++ b/core/src/main/java/coffee/liz/lambda/eval/NormalOrderEvaluator.java
@@ -11,10 +11,7 @@ import coffee.liz.lambda.eval.Value.Closure;
import coffee.liz.lambda.eval.Value.Free;
/**
- * Evaluates lambda expressions using normal order (lazy) reduction.
- *
- * <p>
- * Arguments are wrapped in {@link Thunk} and only evaluated when needed.
+ * Evaluates lambda expressions using normal order reduction.
*/
public final class NormalOrderEvaluator {
diff --git a/core/src/main/java/coffee/liz/lambda/format/Formatter.java b/core/src/main/java/coffee/liz/lambda/format/Formatter.java
index e4fbc0c..ab6f4db 100644
--- a/core/src/main/java/coffee/liz/lambda/format/Formatter.java
+++ b/core/src/main/java/coffee/liz/lambda/format/Formatter.java
@@ -15,9 +15,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
-/**
- * Formats lambda calculus ASTs.
- */
+/** Formats lambda calculus ASTs. */
@RequiredArgsConstructor
public final class Formatter {
@@ -62,7 +60,7 @@ public final class Formatter {
&& comment.span().startColumn() > program.expression().span().startColumn();
if (!isTrailingComment) {
- inlinePartAlreadyOutput = previousEnd != null && comment.isInlineAfter(previousEnd);
+ inlinePartAlreadyOutput = comment.isInlineAfter(previousEnd);
final String leadingPart = getLeadingCommentPart(comment, previousEnd);
if (!leadingPart.isEmpty()) {
@@ -94,7 +92,7 @@ public final class Formatter {
* element).
*/
private String getInlineCommentPart(final SourceComment comment, final SourceSpan previousEnd) {
- if (previousEnd == null || !comment.isInlineAfter(previousEnd)) {
+ if (!comment.isInlineAfter(previousEnd)) {
return "";
}
final String text = comment.text();
@@ -106,7 +104,7 @@ public final class Formatter {
* Gets the leading portion of a comment (all lines except inline first line).
*/
private String getLeadingCommentPart(final SourceComment comment, final SourceSpan previousEnd) {
- if (previousEnd == null || !comment.isInlineAfter(previousEnd)) {
+ if (!comment.isInlineAfter(previousEnd)) {
return comment.text();
}
final String text = comment.text();
diff --git a/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java b/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java
new file mode 100644
index 0000000..337beed
--- /dev/null
+++ b/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java
@@ -0,0 +1,176 @@
+package coffee.liz.lambda.format;
+
+import coffee.liz.lambda.ast.Expression;
+import coffee.liz.lambda.ast.Expression.AbstractionExpression;
+import coffee.liz.lambda.ast.Expression.ApplicationExpression;
+import coffee.liz.lambda.ast.Expression.IdentifierExpression;
+import coffee.liz.lambda.ast.LambdaProgram;
+import coffee.liz.lambda.ast.Macro;
+import coffee.liz.lambda.ast.SourceCode;
+import coffee.liz.lambda.ast.SourceSpan;
+import coffee.liz.lambda.eval.Environment;
+import coffee.liz.lambda.eval.Value;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+
+/** Emits runtime values back into source code. */
+public final class ValueEmitter {
+ private ValueEmitter() {
+ }
+
+ public static SourceCode emit(final Value value, final SourceCode.Syntax syntax) {
+ final ProgramFragment fragment = toProgramFragment(value, new NameGenerator());
+ final LambdaProgram program = new LambdaProgram(SourceSpan.UNKNOWN, fragment.macros(), fragment.expression());
+ final String rendered = Formatter.emit(program, syntax);
+ return switch (syntax) {
+ case LAMBDA -> SourceCode.ofLambda(rendered);
+ case ARROW -> SourceCode.ofArrow(rendered);
+ };
+ }
+
+ private static ProgramFragment toProgramFragment(final Value value, final NameGenerator nameGenerator) {
+ return switch (value) {
+ case Value.Free(String name) -> new ProgramFragment(List.of(), identifier(name));
+ case Value.Application(Value function, Value argument) -> {
+ final ProgramFragment functionFragment = toProgramFragment(function, nameGenerator);
+ final ProgramFragment argumentFragment = toProgramFragment(argument, nameGenerator);
+ final List<Macro> macros = new ArrayList<>(functionFragment.macros());
+ macros.addAll(argumentFragment.macros());
+ yield new ProgramFragment(macros,
+ application(functionFragment.expression(), argumentFragment.expression()));
+ }
+ case Value.Closure(Environment env, String parameter, Expression body) ->
+ emitClosure(env, parameter, body, nameGenerator);
+ };
+ }
+
+ private static ProgramFragment emitClosure(final Environment env, final String parameter, final Expression body,
+ final NameGenerator nameGenerator) {
+ final LinkedHashMap<String, Supplier<Value>> captures = captures(env);
+ if (captures.isEmpty()) {
+ return new ProgramFragment(List.of(), abstraction(parameter, sanitizeExpression(body)));
+ }
+
+ final Set<String> reservedNames = collectIdentifiers(body);
+ final Map<String, ProgramFragment> captureFragments = new LinkedHashMap<>();
+ for (final Map.Entry<String, Supplier<Value>> capture : captures.entrySet()) {
+ final ProgramFragment fragment = toProgramFragment(capture.getValue().get(), nameGenerator);
+ captureFragments.put(capture.getKey(), fragment);
+ reservedNames.addAll(collectIdentifiers(fragment.expression()));
+ fragment.macros().forEach(macro -> reservedNames.add(macro.name()));
+ }
+
+ final Map<String, String> renamedCaptures = new HashMap<>();
+ for (final String captureName : captures.keySet()) {
+ final String generated = nameGenerator.freshCaptureName(reservedNames);
+ reservedNames.add(generated);
+ renamedCaptures.put(captureName, generated);
+ }
+
+ final List<Macro> macros = new ArrayList<>();
+ for (final String captureName : captures.keySet()) {
+ final ProgramFragment fragment = captureFragments.get(captureName);
+ macros.addAll(fragment.macros());
+ macros.add(new Macro(Optional.empty(), SourceSpan.UNKNOWN, renamedCaptures.get(captureName),
+ renameFreeIdentifiers(fragment.expression(), renamedCaptures, Set.of())));
+ }
+
+ final Set<String> bound = new HashSet<>();
+ bound.add(parameter);
+ final Expression rewrittenBody = renameFreeIdentifiers(sanitizeExpression(body), renamedCaptures, bound);
+ return new ProgramFragment(macros, abstraction(parameter, rewrittenBody));
+ }
+
+ private static LinkedHashMap<String, Supplier<Value>> captures(final Environment env) {
+ final LinkedHashMap<String, Supplier<Value>> captures = new LinkedHashMap<>();
+ for (final Environment.BindingFrame frame : env.bindingFrames()) {
+ captures.putIfAbsent(frame.name(), frame.value());
+ }
+ return captures;
+ }
+
+ private static Expression renameFreeIdentifiers(final Expression expression, final Map<String, String> renameMap,
+ final Set<String> boundNames) {
+ return switch (expression) {
+ case IdentifierExpression(var _, var _, String name) -> {
+ if (boundNames.contains(name) || !renameMap.containsKey(name)) {
+ yield identifier(name);
+ }
+ yield identifier(renameMap.get(name));
+ }
+ case ApplicationExpression(var _, var _, Expression applicable, Expression argument) ->
+ application(renameFreeIdentifiers(applicable, renameMap, boundNames),
+ renameFreeIdentifiers(argument, renameMap, boundNames));
+ case AbstractionExpression(var _, var _, String parameter, Expression body) -> {
+ final Set<String> nestedBound = new HashSet<>(boundNames);
+ nestedBound.add(parameter);
+ yield abstraction(parameter, renameFreeIdentifiers(body, renameMap, nestedBound));
+ }
+ };
+ }
+
+ private static Expression sanitizeExpression(final Expression expression) {
+ return switch (expression) {
+ case IdentifierExpression(var _, var _, String name) -> identifier(name);
+ case ApplicationExpression(var _, var _, Expression applicable, Expression argument) ->
+ application(sanitizeExpression(applicable), sanitizeExpression(argument));
+ case AbstractionExpression(var _, var _, String parameter, Expression body) ->
+ abstraction(parameter, sanitizeExpression(body));
+ };
+ }
+
+ private static Set<String> collectIdentifiers(final Expression expression) {
+ final Set<String> identifiers = new HashSet<>();
+ collectIdentifiers(expression, identifiers);
+ return identifiers;
+ }
+
+ private static void collectIdentifiers(final Expression expression, final Set<String> identifiers) {
+ switch (expression) {
+ case IdentifierExpression(var _, var _, String name) -> identifiers.add(name);
+ case ApplicationExpression(var _, var _, Expression applicable, Expression argument) -> {
+ collectIdentifiers(applicable, identifiers);
+ collectIdentifiers(argument, identifiers);
+ }
+ case AbstractionExpression(var _, var _, String parameter, Expression body) -> {
+ identifiers.add(parameter);
+ collectIdentifiers(body, identifiers);
+ }
+ }
+ }
+
+ private static IdentifierExpression identifier(final String name) {
+ return new IdentifierExpression(Optional.empty(), SourceSpan.UNKNOWN, name);
+ }
+
+ private static ApplicationExpression application(final Expression function, final Expression argument) {
+ return new ApplicationExpression(Optional.empty(), SourceSpan.UNKNOWN, function, argument);
+ }
+
+ private static AbstractionExpression abstraction(final String parameter, final Expression body) {
+ return new AbstractionExpression(Optional.empty(), SourceSpan.UNKNOWN, parameter, body);
+ }
+
+ private record ProgramFragment(List<Macro> macros, Expression expression) {
+ }
+
+ private static final class NameGenerator {
+ private int nextCapture = 0;
+
+ private String freshCaptureName(final Set<String> reservedNames) {
+ String candidate;
+ do {
+ candidate = "_cap" + nextCapture++;
+ } while (reservedNames.contains(candidate));
+ return candidate;
+ }
+ }
+}
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 ="));
+ }
+}