diff options
| author | Elizabeth Hunt <me@liz.coffee> | 2026-02-15 10:08:30 -0800 |
|---|---|---|
| committer | Elizabeth Hunt <me@liz.coffee> | 2026-02-15 10:12:08 -0800 |
| commit | d6a294e14604aecd9046cbc19dc1837ca694fbb0 (patch) | |
| tree | 616e87e59f9ee95253acce05aa26405523a9acd7 /core/src | |
| parent | 347564c592325007cb81dbdf1a07831c2ae003c3 (diff) | |
| download | the-abstraction-engine-java-d6a294e14604aecd9046cbc19dc1837ca694fbb0.tar.gz the-abstraction-engine-java-d6a294e14604aecd9046cbc19dc1837ca694fbb0.zip | |
feat: Adding ValueEmitter, LambdaEvaluatorSystem
Diffstat (limited to 'core/src')
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 =")); + } +} |
