aboutsummaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
Diffstat (limited to 'core')
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/Theme.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/actor/Abstraction.java228
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridInterpolationSystem.java13
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/GridWorldBridgeSystem.java25
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/screen/game/system/RenderSystem.java3
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java192
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java31
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/context/KeyContext.java10
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java38
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/NormalModeHandler.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java7
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java2
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java19
-rw-r--r--core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java52
-rw-r--r--core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java58
-rw-r--r--core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java43
-rw-r--r--core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java34
18 files changed, 645 insertions, 117 deletions
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/Theme.java b/core/src/main/java/coffee/liz/abstractionengine/app/Theme.java
index 1d3f449..11837c6 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/Theme.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/Theme.java
@@ -24,6 +24,9 @@ public final class Theme {
public static final Color SECONDARY_LIGHT = Color.valueOf("d7c8ff");
public static final Color SECONDARY_DARK = Color.valueOf("8f78d6");
+ public static final Color DEPENDENCY_PIPE = Color.valueOf("73dbff");
+ public static final Color DEPENDENCY_DESTINATION = Color.valueOf("73dbff");
+
private Theme() {
}
}
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 86a573c..88eb462 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
@@ -1,36 +1,67 @@
package coffee.liz.abstractionengine.app.actor;
+import coffee.liz.abstractionengine.app.Theme;
+import coffee.liz.abstractionengine.app.ui.editor.LambdaEditor;
+import coffee.liz.abstractionengine.app.utils.FontHelper;
+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.math.Vec2f;
import coffee.liz.ecs.math.Vec2i;
import coffee.liz.ecs.model.Entity;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Batch;
+import com.badlogic.gdx.graphics.g2d.BitmapFont;
+import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
+import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.Null;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
+import coffee.liz.lambda.ast.SourceCode;
+
+import java.util.Optional;
+import java.util.List;
+import java.util.stream.IntStream;
@Log4j2
public class Abstraction extends Actor {
private static final Vec2<Integer> ABSTRACTION_FRAMES = Vec2i.builder().x(1).y(4).build();
+ private static final float CONNECTOR_WIDTH = 4f;
+ private static final float DESTINATION_ALPHA = 0.34f;
+ private static final Color PIPE_COLOR = Theme.DEPENDENCY_PIPE;
+ private static final Color DESTINATION_COLOR = Theme.DEPENDENCY_DESTINATION;
+ private static final BitmapFont DEPENDENCY_FONT = FontHelper.initFont(16, FontHelper.MONO, 0.75f);
+ private static final GlyphLayout TEXT_LAYOUT = new GlyphLayout();
+ private static Texture whitePixel;
@Getter
private final Animation<TextureRegion> animation;
- // private final Entity abstractionEntity;
+ private final Entity abstractionEntity;
+ private final Vec2<Float> gridCellSize;
+ private final Stage uiStage;
+ private final boolean vimMode;
- private boolean isActive;
private float stateTime = 0f;
+ private float dependencyConnectorProgress = 0f;
+ private LambdaEditor editorWindow;
+
+ public Abstraction(final Entity abstractionEntity, final Vec2<Float> gridCellSize, final Stage uiStage,
+ final boolean vimMode) {
+ this.abstractionEntity = abstractionEntity;
+ this.gridCellSize = gridCellSize;
+ this.uiStage = uiStage;
+ this.vimMode = vimMode;
+ initWhitePixel();
- /**
- * Creates a penguin 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());
@@ -41,32 +72,33 @@ public class Abstraction extends Actor {
@Override
public void enter(final InputEvent event, final float x, final float y, final int pointer,
@Null final Actor fromActor) {
- log.info("Entered mouse {}", event);
+ Abstraction.this.onMouseEnter();
+ }
+
+ @Override
+ public void exit(final InputEvent event, final float x, final float y, final int pointer,
+ @Null final Actor fromActor) {
+ Abstraction.this.onMouseExit();
}
@Override
public boolean touchDown(final InputEvent event, final float x, final float y, final int pointer,
final int button) {
- log.info("TouchDown!!!");
- return false;
+ Abstraction.this.onMouseClick();
+ return true;
}
});
}
- /**
- * Actor draw hook.
- *
- * @param batch
- * parent batch
- * @param parentAlpha
- * parent alpha multiplier
- */
@Override
public void draw(final Batch batch, final float parentAlpha) {
+ final Color previousBatchColor = batch.getColor().cpy();
final TextureRegion frame = this.animation.getKeyFrame(stateTime, true);
final Color c = getColor();
batch.setColor(c.r, c.g, c.b, c.a * parentAlpha);
batch.draw(frame, getX(), getY(), getWidth(), getHeight());
+ drawDependencyOverlay(batch, parentAlpha);
+ batch.setColor(previousBatchColor);
}
@Override
@@ -74,4 +106,166 @@ public class Abstraction extends Actor {
super.act(delta);
stateTime += delta;
}
+
+ private void onMouseEnter() {
+ dependencyConnectorProgress = 1f;
+ }
+
+ private void onMouseExit() {
+ dependencyConnectorProgress = 0f;
+ }
+
+ private void onMouseClick() {
+ if (!abstractionEntity.has(LambdaTerm.class) || uiStage == null) {
+ return;
+ }
+
+ if (editorWindow != null && editorWindow.getParent() != null) {
+ editorWindow.toFront();
+ uiStage.setKeyboardFocus(editorWindow.getEditorTextArea());
+ return;
+ }
+
+ final SourceCode term = abstractionEntity.get(LambdaTerm.class).getTerm();
+ editorWindow = new LambdaEditor(vimMode, term, this::onTermSaved);
+ uiStage.addActor(editorWindow);
+ editorWindow.centerWindow();
+ uiStage.setKeyboardFocus(editorWindow.getEditorTextArea());
+ }
+
+ private void onTermSaved(final SourceCode updatedTerm) {
+ abstractionEntity.add(new LambdaTerm(updatedTerm));
+ }
+
+ private void drawDependencyOverlay(final Batch batch, final float parentAlpha) {
+ if (dependencyConnectorProgress <= 0f) {
+ return;
+ }
+ if (!abstractionEntity.has(GridPosition.class)) {
+ return;
+ }
+
+ final List<Vec2<Integer>> dependencies = Optional.of(abstractionEntity)
+ .filter(e -> e.has(Substitution.class))
+ .map(e -> e.get(Substitution.class).getTermDependencies())
+ .orElseGet(List::of);
+ if (dependencies.isEmpty()) {
+ return;
+ }
+
+ final Vec2<Float> sourceCenter = Vec2f.builder().x(getX()).y(getY()).build()
+ .plus(gridCellSize.scale(0.5f, 0.5f));
+ final float alpha = getColor().a * parentAlpha;
+ final List<Connector> connectors =
+ IntStream.range(0, dependencies.size()).mapToObj(index -> buildConnector(sourceCenter, String.valueOf(index), dependencies.get(index))).toList();
+
+ connectors.forEach(connector -> drawConnector(batch, connector, alpha));
+ connectors.forEach(connector -> drawDependencyDestination(batch, connector.symbol(), connector.destinationGridPosition(),
+ alpha));
+ }
+
+ private void drawConnector(final Batch batch, final Connector connector, final float alpha) {
+ final Vec2<Float> start = connector.start();
+ final Vec2<Float> corner = connector.corner();
+ final Vec2<Float> end = connector.end();
+ final Vec2<Float> delta = end.minus(start);
+ final float horizontalLength = Math.abs(delta.getX());
+ final float verticalLength = Math.abs(delta.getY());
+ final float totalLength = horizontalLength + verticalLength;
+ if (totalLength <= 0.001f) {
+ return;
+ }
+
+ float remaining = totalLength * dependencyConnectorProgress;
+ final float consumedHorizontal = drawPartialAxisAlignedSegment(batch, start, corner, remaining, alpha);
+ remaining -= consumedHorizontal;
+ if (remaining <= 0f) {
+ return;
+ }
+
+ drawPartialAxisAlignedSegment(batch, corner, end, remaining, alpha);
+ }
+
+ private float drawPartialAxisAlignedSegment(final Batch batch, final Vec2<Float> start, final Vec2<Float> end,
+ final float availableLength, final float alpha) {
+ final Vec2<Float> delta = end.minus(start);
+ final float length = Math.abs(delta.getX()) + Math.abs(delta.getY());
+ if (length <= 0.001f || availableLength <= 0f) {
+ return 0f;
+ }
+
+ final float consumed = Math.min(length, availableLength);
+ final boolean horizontal = Math.abs(delta.getY()) < 0.001f;
+ final float direction = horizontal ? Math.signum(delta.getX()) : Math.signum(delta.getY());
+ final float drawLength = consumed * direction;
+
+ batch.setColor(PIPE_COLOR.r, PIPE_COLOR.g, PIPE_COLOR.b, alpha);
+ if (horizontal) {
+ final float drawX = direction > 0 ? start.getX() : start.getX() + drawLength;
+ batch.draw(whitePixel, drawX, start.getY() - CONNECTOR_WIDTH / 2f, Math.abs(drawLength), CONNECTOR_WIDTH);
+ } else {
+ final float drawY = direction > 0 ? start.getY() : start.getY() + drawLength;
+ batch.draw(whitePixel, start.getX() - CONNECTOR_WIDTH / 2f, drawY, CONNECTOR_WIDTH, Math.abs(drawLength));
+ }
+ return consumed;
+ }
+
+ private Vec2<Float> sourceConnectorAnchor(final Vec2<Float> sourceCenter, final Vec2<Float> travel) {
+ final Vec2<Float> halfCell = gridCellSize.scale(0.5f, 0.5f);
+ if (Math.abs(travel.getX()) > 0.001f) {
+ return sourceCenter.plus(Vec2f.builder().x(Math.signum(travel.getX()) * halfCell.getX()).y(0f).build());
+ }
+ return sourceCenter.plus(Vec2f.builder().x(0f).y(Math.signum(travel.getY()) * halfCell.getY()).build());
+ }
+
+ private Vec2<Float> destinationConnectorAnchor(final Vec2<Float> destinationCenter, final Vec2<Float> travel) {
+ final Vec2<Float> halfCell = gridCellSize.scale(0.5f, 0.5f);
+ if (Math.abs(travel.getY()) > 0.001f) {
+ return destinationCenter.plus(Vec2f.builder().x(0f).y(-Math.signum(travel.getY()) * halfCell.getY()).build());
+ }
+ return destinationCenter.plus(Vec2f.builder().x(-Math.signum(travel.getX()) * halfCell.getX()).y(0f).build());
+ }
+
+ private Connector buildConnector(final Vec2<Float> sourceCenter, final String symbol,
+ final Vec2<Integer> destinationGridPosition) {
+ final Vec2<Float> destinationCenter = destinationGridPosition.floatValue().scale(gridCellSize)
+ .plus(gridCellSize.scale(0.5f, 0.5f));
+ final Vec2<Float> travel = destinationCenter.minus(sourceCenter);
+ final Vec2<Float> start = sourceConnectorAnchor(sourceCenter, travel);
+ final Vec2<Float> end = destinationConnectorAnchor(destinationCenter, travel);
+ final Vec2<Float> corner = Vec2f.builder().x(end.getX()).y(start.getY()).build();
+ return new Connector(start, corner, end, symbol, destinationGridPosition);
+ }
+
+ private void drawDependencyDestination(final Batch batch, final String symbol, final Vec2<Integer> dependencyPosition,
+ final float alpha) {
+ final Vec2<Float> cellOrigin = dependencyPosition.floatValue().scale(gridCellSize);
+
+ batch.setColor(DESTINATION_COLOR.r, DESTINATION_COLOR.g, DESTINATION_COLOR.b, DESTINATION_ALPHA * alpha);
+ batch.draw(whitePixel, cellOrigin.getX(), cellOrigin.getY(), gridCellSize.getX(), gridCellSize.getY());
+
+ TEXT_LAYOUT.setText(DEPENDENCY_FONT, symbol);
+ final Vec2<Float> textPosition = cellOrigin.plus(Vec2f.builder()
+ .x((gridCellSize.getX() - TEXT_LAYOUT.width) / 2f)
+ .y((gridCellSize.getY() + TEXT_LAYOUT.height) / 2f)
+ .build());
+ batch.setColor(1f, 1f, 1f, 1f);
+ DEPENDENCY_FONT.setColor(PIPE_COLOR.r, PIPE_COLOR.g, PIPE_COLOR.b, alpha);
+ DEPENDENCY_FONT.draw(batch, symbol, textPosition.getX(), textPosition.getY());
+ }
+
+ private static void initWhitePixel() {
+ if (whitePixel != null) {
+ return;
+ }
+ final Pixmap pixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
+ pixmap.setColor(Color.WHITE);
+ pixmap.fill();
+ whitePixel = new Texture(pixmap);
+ pixmap.dispose();
+ }
+
+ private record Connector(Vec2<Float> start, Vec2<Float> corner, Vec2<Float> end, String symbol,
+ Vec2<Integer> destinationGridPosition) {
+ }
}
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 d0acfbc..1aa5177 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
@@ -17,6 +17,7 @@ import java.util.Collection;
import java.util.Set;
import com.badlogic.gdx.math.Interpolation;
+import com.badlogic.gdx.scenes.scene2d.Action;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.actions.MoveToAction;
import lombok.extern.log4j.Log4j2;
@@ -36,7 +37,13 @@ public class GridInterpolationSystem implements System<FrameState> {
world.resolve(Query.allOf(ExternalWorldEntityRef.class, ActorHolder.class)).forEach(entity -> {
final Actor actor = entity.get(ActorHolder.class).getActor();
- final boolean isAlreadyInterpolating = !actor.getActions().isEmpty();
+ boolean isAlreadyInterpolating = false;
+ for (final Action action : actor.getActions()) {
+ if (isMoveAction(action)) {
+ isAlreadyInterpolating = true;
+ break;
+ }
+ }
if (isAlreadyInterpolating) {
return;
}
@@ -68,4 +75,8 @@ public class GridInterpolationSystem implements System<FrameState> {
default -> new Interpolation.Swing(1.15f);
};
}
+
+ private boolean isMoveAction(final Action action) {
+ return action instanceof MoveToAction;
+ }
}
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 3d76853..ff977c1 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
@@ -14,18 +14,22 @@ 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.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.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 coffee.liz.lambda.ast.SourceCode;
import lombok.Getter;
import java.time.Duration;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -37,6 +41,7 @@ public class GridWorldBridgeSystem implements System<FrameState> {
private final Map<Integer, Entity> outerIdToInnerEntity = new HashMap<>();
private final AbstractionEngineGridWorld outerWorld;
private final Vec2<Integer> gridSize;
+ private final boolean vimMode;
@Getter
private final Vec2<Float> gridToWorld;
@@ -45,6 +50,7 @@ public class GridWorldBridgeSystem implements System<FrameState> {
public GridWorldBridgeSystem(final Vec2<Integer> gridSize, final Settings settings) {
this.gridSize = gridSize;
+ this.vimMode = settings.isVimMode();
this.outerWorld = new AbstractionEngineGridWorld(gridSize, settings.getPreferredSyntax());
this.gridToWorld = AbstractionEngineGame.WORLD_SIZE
@@ -56,8 +62,11 @@ public class GridWorldBridgeSystem implements System<FrameState> {
public void initLevel() {
EntityFactory.populateRect(gridSize, outerWorld, EntityFactory.Wall::addToWorld);
EntityFactory.Player.addToWorld(outerWorld, Vec2i.builder().y(10).x(10).build());
- EntityFactory.Abstraction.addToWorld(outerWorld, Vec2i.builder().y(10).x(12).build());
- EntityFactory.Abstraction.addToWorld(outerWorld, Vec2i.builder().y(10).x(13).build());
+ EntityFactory.Abstraction.addToWorld(outerWorld, Vec2i.builder().y(10).x(12).build())
+ .add(new LambdaTerm(SourceCode.ofArrow("(f -> x -> f(x))(_)")))
+ .add(new Substitution(Vec2i.NORTH, List.of(Vec2i.builder().x(11).y(11).build())));
+ EntityFactory.Abstraction.addToWorld(outerWorld, Vec2i.builder().y(10).x(13).build())
+ .add(new LambdaTerm(SourceCode.ofArrow("(y -> y)")));
outerWorld.update(GridInputState.builder().movement(Vec2i.ZERO).build());
}
@@ -87,8 +96,9 @@ public class GridWorldBridgeSystem implements System<FrameState> {
}
final Entity outer = world.createEntity();
+ final RenderSystem renderSystem = world.getSystem(RenderSystem.class);
outer.add(new ActorHolder(
- createActor(gridEntity.get(EntityType.class), world.getSystem(RenderSystem.class).getStage())));
+ createActor(gridEntity, renderSystem.getStage(), renderSystem.getUiStage())));
outer.add(new ExternalWorldEntityRef(gridEntity));
outer.add(gridEntity.get(EntityType.class));
outerIdToInnerEntity.put(gridEntity.getId(), outer);
@@ -108,14 +118,17 @@ public class GridWorldBridgeSystem implements System<FrameState> {
FunctionUtils.wrapCheckedException(outerWorld::dispose);
}
- private Actor createActor(final EntityType type, final Stage stage) {
+ private Actor createActor(final Entity gridEntity, final Stage stage, final Stage uiStage) {
+ final EntityType type = gridEntity.get(EntityType.class);
final Actor actor = switch (type) {
case PLAYER -> new Penguin();
- case ABSTRACTION -> new Abstraction();
+ case ABSTRACTION -> new Abstraction(gridEntity, getGridToWorld(), uiStage, vimMode);
case WALL -> new Wall();
default -> throw new UnsupportedOperationException();
};
- actor.setBounds(0, 0, getGridToWorld().getX(), getGridToWorld().getX());
+ final GridPosition position = gridEntity.get(GridPosition.class);
+ final Vec2<Float> worldSpawnPosition = getGridToWorld().scale(position.getPosition().floatValue());
+ actor.setBounds(worldSpawnPosition.getX(), worldSpawnPosition.getY(), getGridToWorld().getX(), getGridToWorld().getX());
stage.addActor(actor);
return actor;
}
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 32b9ebd..a6768c7 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
@@ -3,7 +3,6 @@ package coffee.liz.abstractionengine.app.screen.game.system;
import coffee.liz.abstractionengine.app.Theme;
import coffee.liz.abstractionengine.app.config.Settings;
import coffee.liz.abstractionengine.app.screen.game.FrameState;
-import coffee.liz.abstractionengine.app.ui.editor.LambdaEditor;
import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.model.System;
import coffee.liz.ecs.model.World;
@@ -27,6 +26,7 @@ public class RenderSystem implements System<FrameState> {
private final ShapeRenderer shapeRenderer;
@Getter
private final Stage stage;
+ @Getter
private final Stage uiStage;
private final InputMultiplexer inputMultiplexer;
@@ -37,7 +37,6 @@ public class RenderSystem implements System<FrameState> {
this.stage = new Stage(viewport);
this.uiStage = new Stage(new ScreenViewport());
- uiStage.addActor(new LambdaEditor(settings.isVimMode()));
// TODO: Move this to an input system
this.inputMultiplexer = new InputMultiplexer(uiStage, stage);
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java
index 11a4b0e..1affd43 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/LambdaEditor.java
@@ -3,78 +3,214 @@ package coffee.liz.abstractionengine.app.ui.editor;
import coffee.liz.abstractionengine.app.Theme;
import coffee.liz.abstractionengine.app.ui.editor.vim.VimEngine;
import coffee.liz.abstractionengine.app.utils.FontHelper;
+import coffee.liz.lambda.LambdaDriver;
+import coffee.liz.lambda.ast.LambdaProgram;
+import coffee.liz.lambda.ast.SourceCode;
+import coffee.liz.lambda.format.Formatter;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
+import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Align;
import com.kotcrab.vis.ui.VisUI;
import com.kotcrab.vis.ui.util.TableUtils;
import com.kotcrab.vis.ui.util.highlight.Highlighter;
import com.kotcrab.vis.ui.widget.VisLabel;
+import com.kotcrab.vis.ui.widget.VisSelectBox;
+import com.kotcrab.vis.ui.widget.VisTable;
+import com.kotcrab.vis.ui.widget.VisTextButton;
import com.kotcrab.vis.ui.widget.VisTextField.VisTextFieldStyle;
import com.kotcrab.vis.ui.widget.VisWindow;
+import lombok.Getter;
+
+import java.util.function.Consumer;
public class LambdaEditor extends VisWindow {
private static final int FONT_SIZE = 15;
private static final float WINDOW_ALPHA = 0.92f;
- private static final String DEFAULT_TEXT = "-- Church numerals\n" + "let zero = λf.λx.x;\n"
- + "let one = λf.λx.f x;\n" + "let succ = λn.λf.λx.f (n f x);\n" + "let add = λm.λn.λf.λx.m f (n f x);\n"
- + "\n" + "succ (add one zero)\n";
-
private final BitmapFont font;
+ private final Consumer<SourceCode> onSave;
+ private final boolean vimMode;
+ private final VisLabel statusLabel;
+ private final VisSelectBox<SourceCode.Syntax> syntaxSwitch;
+ @Getter
+ private final EditorTextArea editorTextArea;
+ private SourceCode.Syntax syntax;
+ private boolean updatingSyntaxSwitch = false;
- public LambdaEditor(final boolean vimMode) {
+ public LambdaEditor(final boolean vimMode, final SourceCode initialSource, final Consumer<SourceCode> onSave) {
super("λ-editor");
+ this.vimMode = vimMode;
+ this.onSave = onSave != null ? onSave : _ -> { };
+ this.syntax = syntaxOf(initialSource);
+ this.font = FontHelper.initFont(FONT_SIZE, FontHelper.MONO, 1f);
+ this.statusLabel = new VisLabel(vimMode ? "-- NORMAL --" : "-- INSERT --");
+ this.syntaxSwitch = new VisSelectBox<>();
TableUtils.setSpacingDefaults(this);
columnDefaults(0).left();
- this.font = FontHelper.initFont(FONT_SIZE, FontHelper.MONO, 1f);
-
setResizable(true);
addCloseButton();
- addEditor(vimMode);
- setSize(400, 300);
- setColor(1, 1, 1, WINDOW_ALPHA);
- centerWindow();
- }
-
- private void addEditor(final boolean vimMode) {
final VisTextFieldStyle style = new VisTextFieldStyle(VisUI.getSkin().get("textArea", VisTextFieldStyle.class));
style.font = font;
final GlyphLayout layout = new GlyphLayout(font, "000 ");
final float gutterWidth = layout.width;
- final EditorTextArea textArea = new EditorTextArea(DEFAULT_TEXT, style, gutterWidth);
- textArea.setHighlighter(createHighlighter());
+ this.editorTextArea = new EditorTextArea(sourceOf(initialSource), style, gutterWidth);
+ editorTextArea.setHighlighter(createHighlighter(syntax));
- final ScrollPane scrollPane = textArea.createCompatibleScrollPane();
- scrollPane.setFadeScrollBars(false);
+ bindInput(vimMode);
+ buildLayout();
+ bindSyntaxSwitch();
+ syntaxSwitch.setSelected(syntax);
- final VisLabel statusLabel = new VisLabel(vimMode ? "-- NORMAL --" : "-- INSERT --");
- statusLabel.setAlignment(Align.left);
+ setSize(560, 380);
+ setColor(1, 1, 1, WINDOW_ALPHA);
+ }
- if (vimMode) {
- final VimEngine controller = new VimEngine(textArea, statusLabel::setText);
- textArea.setInputStrategy(controller);
- statusLabel.setText(controller.getStatusText());
+ private void bindInput(final boolean vimEnabled) {
+ statusLabel.setAlignment(Align.left);
+ if (!vimEnabled) {
+ return;
}
+ final VimEngine controller = new VimEngine(editorTextArea, statusLabel::setText, this::executeExCommand);
+ editorTextArea.setInputStrategy(controller);
+ statusLabel.setText(controller.getStatusText());
+ }
+
+ private void buildLayout() {
+ final ScrollPane scrollPane = editorTextArea.createCompatibleScrollPane();
+ scrollPane.setFadeScrollBars(false);
+
+ final VisTextButton saveButton = new VisTextButton(vimMode ? "Save (:w)" : "Save");
+ saveButton.addListener(new ChangeListener() {
+ @Override
+ public void changed(final ChangeEvent event, final com.badlogic.gdx.scenes.scene2d.Actor actor) {
+ saveCurrentTerm();
+ }
+ });
+
+ final VisTable controls = new VisTable();
+ controls.add(statusLabel).growX().left();
+ controls.add(syntaxSwitch).padLeft(8f);
+ controls.add(saveButton).padLeft(8f);
add(scrollPane).grow().row();
- add(statusLabel).growX().padTop(6f);
+ add(controls).growX().padTop(6f);
+ }
+
+ private void bindSyntaxSwitch() {
+ syntaxSwitch.setItems(SourceCode.Syntax.ARROW, SourceCode.Syntax.LAMBDA);
+ syntaxSwitch.addListener(new ChangeListener() {
+ @Override
+ public void changed(final ChangeEvent event, final com.badlogic.gdx.scenes.scene2d.Actor actor) {
+ if (updatingSyntaxSwitch) {
+ return;
+ }
+ applySyntaxSwitch(syntaxSwitch.getSelected());
+ }
+ });
+ }
+
+ private void applySyntaxSwitch(final SourceCode.Syntax nextSyntax) {
+ if (nextSyntax == syntax) {
+ return;
+ }
+ try {
+ final LambdaProgram program = LambdaDriver.parse(sourceFromEditor());
+ final String formatted = Formatter.emit(program, nextSyntax);
+ syntax = nextSyntax;
+ editorTextArea.setText(formatted);
+ editorTextArea.syncCursorState();
+ editorTextArea.setHighlighter(createHighlighter(nextSyntax));
+ setStatus("Converted to " + nextSyntax.name().toLowerCase() + " syntax");
+ } catch (final RuntimeException parseException) {
+ setStatus("Cannot switch syntax: parse error");
+ updatingSyntaxSwitch = true;
+ syntaxSwitch.setSelected(syntax);
+ updatingSyntaxSwitch = false;
+ }
+ }
+
+ private void executeExCommand(final String command) {
+ final String normalized = command == null ? "" : command.trim();
+ switch (normalized) {
+ case "w" -> saveCurrentTerm();
+ case "wq" -> {
+ if (saveCurrentTerm()) {
+ remove();
+ }
+ }
+ case "q" -> remove();
+ default -> setStatus("Unknown command :" + normalized);
+ }
+ }
+
+ private boolean saveCurrentTerm() {
+ try {
+ final LambdaProgram program = LambdaDriver.parse(sourceFromEditor());
+ final String formatted = Formatter.emit(program, syntax);
+ final SourceCode updatedTerm = sourceCodeForSyntax(formatted, syntax);
+ editorTextArea.setText(formatted);
+ editorTextArea.syncCursorState();
+ onSave.accept(updatedTerm);
+ setStatus("Saved");
+ return true;
+ } catch (final RuntimeException parseException) {
+ setStatus("Save failed: parse error");
+ return false;
+ }
+ }
+
+ private SourceCode sourceFromEditor() {
+ return sourceCodeForSyntax(editorTextArea.getText(), syntax);
}
- private static Highlighter createHighlighter() {
+ private static SourceCode sourceCodeForSyntax(final String source, final SourceCode.Syntax syntax) {
+ return switch (syntax) {
+ case ARROW -> SourceCode.ofArrow(source);
+ case LAMBDA -> SourceCode.ofLambda(source);
+ };
+ }
+
+ private static SourceCode.Syntax syntaxOf(final SourceCode sourceCode) {
+ return switch (sourceCode) {
+ case SourceCode.Arrow ignored -> SourceCode.Syntax.ARROW;
+ case SourceCode.Lambda ignored -> SourceCode.Syntax.LAMBDA;
+ };
+ }
+
+ private static String sourceOf(final SourceCode sourceCode) {
+ return switch (sourceCode) {
+ case SourceCode.Arrow(String source) -> source;
+ case SourceCode.Lambda(String source) -> source;
+ };
+ }
+
+ private void setStatus(final String text) {
+ if (text == null || text.isBlank()) {
+ return;
+ }
+ statusLabel.setText(text);
+ }
+
+ private static Highlighter createHighlighter(final SourceCode.Syntax syntax) {
final Highlighter highlighter = new Highlighter();
highlighter.regex(Theme.MUTED, "--[^\\n]*");
highlighter.regex(Theme.PRIMARY, "\\blet\\b");
- highlighter.regex(Theme.SECONDARY, "[λ\\\\]");
- highlighter.regex(Theme.SECONDARY, "\\.");
highlighter.regex(Theme.BORDER_LIGHT, "[=;]");
highlighter.regex(Theme.BORDER_LIGHT, "[()]");
+ switch (syntax) {
+ case ARROW -> highlighter.regex(Theme.SECONDARY, "->");
+ case LAMBDA -> {
+ highlighter.regex(Theme.SECONDARY, "[λ\\\\]");
+ highlighter.regex(Theme.SECONDARY, "\\.");
+ }
+ }
return highlighter;
}
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java
index ab5f203..d2fc843 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/VimEngine.java
@@ -9,6 +9,7 @@ import coffee.liz.abstractionengine.app.ui.editor.RenderState;
import coffee.liz.abstractionengine.app.ui.editor.EditorTextArea;
import coffee.liz.abstractionengine.app.ui.editor.vim.buffer.TextAreaBuffer;
import coffee.liz.abstractionengine.app.ui.editor.vim.context.KeyContext;
+import coffee.liz.abstractionengine.app.ui.editor.vim.handler.CommandModeHandler;
import coffee.liz.abstractionengine.app.ui.editor.vim.handler.HandleResult;
import coffee.liz.abstractionengine.app.ui.editor.vim.handler.InsertModeHandler;
import coffee.liz.abstractionengine.app.ui.editor.vim.handler.ModeHandler;
@@ -21,6 +22,7 @@ import coffee.liz.abstractionengine.app.ui.editor.vim.model.UndoManager;
import coffee.liz.abstractionengine.app.ui.editor.vim.navigation.TextNavigation;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.InsertState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.ModeState;
+import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.SearchState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualLineState;
@@ -39,38 +41,55 @@ public class VimEngine implements InputStrategy {
private final ModeHandler<VisualState> visualModeHandler;
private final ModeHandler<VisualLineState> visualLineModeHandler;
private final ModeHandler<SearchState> searchModeHandler;
+ private final ModeHandler<CommandState> commandModeHandler;
private ModeState state;
public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink, final VimConfig config) {
- this(new TextAreaBuffer(textArea), statusSink, config);
+ this(new TextAreaBuffer(textArea), statusSink, null, config);
+ }
+
+ public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink,
+ final Consumer<String> commandSink, final VimConfig config) {
+ this(new TextAreaBuffer(textArea), statusSink, commandSink, config);
+ }
+
+ public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink,
+ final Consumer<String> commandSink) {
+ this(new TextAreaBuffer(textArea), statusSink, commandSink, VimConfig.defaults());
}
public VimEngine(final EditorTextArea textArea, final Consumer<String> statusSink) {
- this(textArea, statusSink, VimConfig.defaults());
+ this(textArea, statusSink, null, VimConfig.defaults());
}
public VimEngine(final EditorTextArea textArea) {
- this(textArea, null, VimConfig.defaults());
+ this(textArea, null, null, VimConfig.defaults());
}
public VimEngine(final EditorBuffer buffer, final Consumer<String> statusSink, final VimConfig config) {
+ this(buffer, statusSink, null, config);
+ }
+
+ public VimEngine(final EditorBuffer buffer, final Consumer<String> statusSink, final Consumer<String> commandSink,
+ final VimConfig config) {
this.config = config != null ? config : VimConfig.defaults();
this.context = new KeyContext(buffer, statusSink, new TextNavigation(),
- new UndoManager(this.config.maxUndoStates()), new Register(), this.config);
+ new UndoManager(this.config.maxUndoStates()), new Register(), this.config, commandSink);
this.statusFormatter = new StatusFormatter();
this.normalModeHandler = new NormalModeHandler();
this.insertModeHandler = new InsertModeHandler();
this.visualModeHandler = new VisualModeHandler();
this.visualLineModeHandler = new VisualLineModeHandler();
this.searchModeHandler = new SearchModeHandler();
+ this.commandModeHandler = new CommandModeHandler();
this.state = NormalState.DEFAULT;
context.recordUndoState();
publishStatus();
}
public VimEngine(final EditorBuffer buffer, final Consumer<String> statusSink) {
- this(buffer, statusSink, VimConfig.defaults());
+ this(buffer, statusSink, null, VimConfig.defaults());
}
@Override
@@ -160,6 +179,7 @@ public class VimEngine implements InputStrategy {
case VisualState visualState -> visualModeHandler.onKeyDown(visualState, context);
case VisualLineState visualLineState -> visualLineModeHandler.onKeyDown(visualLineState, context);
case SearchState searchState -> searchModeHandler.onKeyDown(searchState, context);
+ case CommandState commandState -> commandModeHandler.onKeyDown(commandState, context);
};
}
@@ -171,6 +191,7 @@ public class VimEngine implements InputStrategy {
case VisualLineState visualLineState ->
visualLineModeHandler.onKeyTyped(visualLineState, character, context);
case SearchState searchState -> searchModeHandler.onKeyTyped(searchState, character, context);
+ case CommandState commandState -> commandModeHandler.onKeyTyped(commandState, character, context);
};
}
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 285bbde..e269b58 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
@@ -33,6 +33,7 @@ public class KeyContext {
private final UndoManager undoManager;
private final Register register;
private final VimConfig config;
+ private final Consumer<String> commandExecutor;
@Getter
private String statusText;
@@ -48,13 +49,14 @@ public class KeyContext {
public KeyContext(final EditorBuffer editorBuffer, final Consumer<String> statusUpdater,
final TextNavigation navigation, final UndoManager undoManager, final Register register,
- final VimConfig config) {
+ final VimConfig config, final Consumer<String> commandExecutor) {
this.editorBuffer = editorBuffer;
this.statusUpdater = statusUpdater;
this.navigation = navigation;
this.undoManager = undoManager;
this.register = register;
this.config = config;
+ this.commandExecutor = commandExecutor;
this.statusText = "";
this.lastSearchQuery = "";
this.lastSearchDirection = SearchDirection.FORWARD;
@@ -66,6 +68,12 @@ public class KeyContext {
this.currentKeyEvent = new KeyEvent(Input.Keys.UNKNOWN, Set.of());
}
+ public void executeCommand(final String command) {
+ if (commandExecutor != null) {
+ commandExecutor.accept(command == null ? "" : command);
+ }
+ }
+
public EditorBuffer editorBuffer() {
return editorBuffer;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java
new file mode 100644
index 0000000..fea2f8a
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/handler/CommandModeHandler.java
@@ -0,0 +1,38 @@
+package coffee.liz.abstractionengine.app.ui.editor.vim.handler;
+
+import coffee.liz.abstractionengine.app.ui.editor.vim.context.KeyContext;
+import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState;
+import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState;
+import com.badlogic.gdx.Input;
+
+public class CommandModeHandler implements ModeHandler<CommandState> {
+ @Override
+ public HandleResult onKeyDown(final CommandState state, final KeyContext context) {
+ if (context.currentKeyEvent().keycode() == Input.Keys.ESCAPE) {
+ return HandleResult.handled(NormalState.DEFAULT);
+ }
+ return HandleResult.handled();
+ }
+
+ @Override
+ public HandleResult onKeyTyped(final CommandState state, final char character, final KeyContext context) {
+ if (character == '\r' || character == '\n') {
+ context.executeCommand(state.command().toString());
+ return HandleResult.handled(NormalState.DEFAULT);
+ }
+
+ if (character == '\b') {
+ if (state.command().length() > 0) {
+ state.command().setLength(state.command().length() - 1);
+ }
+ return HandleResult.handled(state);
+ }
+
+ if (character < 32 || character == 127) {
+ return HandleResult.handled(state);
+ }
+
+ state.command().append(character);
+ return HandleResult.handled(state);
+ }
+}
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 95dc2bc..3fc24d3 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
@@ -6,6 +6,7 @@ import coffee.liz.abstractionengine.app.ui.editor.vim.search.SearchDirection;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.InsertState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.PendingOperator;
+import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.SearchState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualLineState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.VisualState;
@@ -296,6 +297,7 @@ public class NormalModeHandler implements ModeHandler<NormalState> {
return HandleResult.handled(new NormalState(0, PendingOperator.NONE, state.preferredColumn()));
});
map.put('u', (state, context) -> HandleResult.handled(context.undoToNormalState()));
+ map.put(':', (state, context) -> HandleResult.handled(new CommandState(new StringBuilder())));
return map;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java
new file mode 100644
index 0000000..e0112e0
--- /dev/null
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/CommandState.java
@@ -0,0 +1,7 @@
+package coffee.liz.abstractionengine.app.ui.editor.vim.state;
+
+public record CommandState(StringBuilder command) implements ModeState {
+ public CommandState {
+ command = command == null ? new StringBuilder() : command;
+ }
+}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java
index e878dd7..20ce01e 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/state/ModeState.java
@@ -1,6 +1,6 @@
package coffee.liz.abstractionengine.app.ui.editor.vim.state;
-public sealed interface ModeState permits InsertState, NormalState, SearchState, VisualLineState, VisualState {
+public sealed interface ModeState permits CommandState, InsertState, NormalState, SearchState, VisualLineState, VisualState {
default PendingOperator pendingOperator() {
return PendingOperator.NONE;
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java
index 5ff2654..d38f9d6 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/app/ui/editor/vim/status/StatusFormatter.java
@@ -3,6 +3,7 @@ package coffee.liz.abstractionengine.app.ui.editor.vim.status;
import coffee.liz.abstractionengine.app.ui.editor.vim.VimConfig;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.InsertState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.ModeState;
+import coffee.liz.abstractionengine.app.ui.editor.vim.state.CommandState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.NormalState;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.PendingOperator;
import coffee.liz.abstractionengine.app.ui.editor.vim.state.SearchState;
@@ -17,6 +18,7 @@ public class StatusFormatter {
case VisualState ignored -> config.visualStatusLabel();
case VisualLineState ignored -> config.visualLineStatusLabel();
case SearchState searchState -> '/' + searchState.query().toString();
+ case CommandState commandState -> ':' + commandState.command().toString();
};
}
diff --git a/core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java b/core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java
index 77ff4e8..9f58ff2 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/component/Substitution.java
@@ -2,14 +2,29 @@ package coffee.liz.abstractionengine.grid.component;
import coffee.liz.ecs.math.Vec2;
import coffee.liz.ecs.model.Component;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
-import java.util.Map;
+import java.util.List;
@RequiredArgsConstructor
+@AllArgsConstructor
@Data
+@Builder(toBuilder = true)
public class Substitution implements Component {
+ /**
+ * Produced terms spawn with this momentum.
+ */
private final Vec2<Integer> spawnMomentum;
- private final Map<String, Vec2<Integer>> termDependencies;
+
+ /**
+ * Left-to-right dependencies' grid positions to apply in order.
+ * i.e. [(0, 0), (0, 1)] would apply lambda term at (0, 0) then (0, 1) to a -> b -> a => (0, 0)
+ */
+ private final List<Vec2<Integer>> termDependencies;
+
+ @Builder.Default
+ private boolean isActive = true;
}
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
index 74bc3fc..f93d60b 100644
--- a/core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java
+++ b/core/src/main/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystem.java
@@ -8,12 +8,16 @@ 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.Entity;
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.LambdaProgram;
import coffee.liz.lambda.ast.SourceCode;
import coffee.liz.lambda.eval.Environment;
+import coffee.liz.lambda.eval.NormalOrderEvaluator;
+import coffee.liz.lambda.eval.Thunk;
import coffee.liz.lambda.eval.Value;
import coffee.liz.lambda.format.ValueEmitter;
import lombok.extern.log4j.Log4j2;
@@ -21,10 +25,7 @@ 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;
+import java.util.Objects;
@Log4j2
public class LambdaEvaluatorSystem implements System<GridInputState> {
@@ -45,38 +46,39 @@ public class LambdaEvaluatorSystem implements System<GridInputState> {
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 Substitution substitution = entity.get(Substitution.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 List<Entity> resolvedDependencies = substitution.getTermDependencies().stream()
+ .map(dependencyPos -> gridIndexSystem.entitiesAt(dependencyPos).stream()
+ .filter(_entity -> _entity.has(LambdaTerm.class))
+ .findFirst()
+ .orElse(null))
+ .toList();
- final boolean dependenciesSatisified = dependencies.keySet()
- .equals(substitution.getTermDependencies().keySet());
+ final boolean dependenciesSatisified = resolvedDependencies.stream().allMatch(Objects::nonNull);
if (!dependenciesSatisified) {
+ entity.add(substitution.toBuilder().isActive(true).build());
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);
+ if (!substitution.isActive()) {
+ return;
+ }
+
+ Value result = LambdaDriver.interpret(term.getTerm());
+ for (final Entity dependency : resolvedDependencies) {
+ result = new Value.Application(result, LambdaDriver.interpret(dependency.get(LambdaTerm.class).getTerm()));
+ }
+
+ final SourceCode emittedResult = ValueEmitter.emit(LambdaDriver.interpret(ValueEmitter.emit(result, preferredSyntax)), preferredSyntax);
final Vec2<Integer> spawnMomentum = substitution.getSpawnMomentum();
- EntityFactory.Abstraction.addToWorld(world, position.getPosition(), new LambdaTerm(emittedResult))
+ final Entity producedTerm = EntityFactory.Abstraction.addToWorld(world, position.getPosition(), new LambdaTerm(emittedResult))
.add(EntityType.ABSTRACTION).add(new GridPosition(position.getPosition()))
.add(new GridMomentum(spawnMomentum));
+ entity.add(substitution.toBuilder().isActive(false).build());
+ log.info("Entity {} produced {}", entity, producedTerm);
});
}
}
diff --git a/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java b/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java
index 337beed..2c844c9 100644
--- a/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java
+++ b/core/src/main/java/coffee/liz/lambda/format/ValueEmitter.java
@@ -54,24 +54,39 @@ public final class ValueEmitter {
private static ProgramFragment emitClosure(final Environment env, final String parameter, final Expression body,
final NameGenerator nameGenerator) {
- final LinkedHashMap<String, Supplier<Value>> captures = captures(env);
+ final Expression sanitizedBody = sanitizeExpression(body);
+ final Set<String> referencedNames = freeIdentifiers(sanitizedBody, Set.of(parameter));
+ final LinkedHashMap<String, Supplier<Value>> captures = captures(env, referencedNames);
if (captures.isEmpty()) {
- return new ProgramFragment(List.of(), abstraction(parameter, sanitizeExpression(body)));
+ return new ProgramFragment(List.of(), abstraction(parameter, sanitizedBody));
}
- final Set<String> reservedNames = collectIdentifiers(body);
+ final Set<String> reservedNames = collectIdentifiers(sanitizedBody);
final Map<String, ProgramFragment> captureFragments = new LinkedHashMap<>();
+ final Set<String> reservedMacroNames = new HashSet<>();
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()));
+ fragment.macros().forEach(macro -> {
+ reservedNames.add(macro.name());
+ reservedMacroNames.add(macro.name());
+ });
}
final Map<String, String> renamedCaptures = new HashMap<>();
+ final Set<String> chosenCaptureNames = new HashSet<>();
for (final String captureName : captures.keySet()) {
- final String generated = nameGenerator.freshCaptureName(reservedNames);
+ final boolean clashes = reservedMacroNames.contains(captureName) || chosenCaptureNames.contains(captureName);
+ if (!clashes) {
+ renamedCaptures.put(captureName, captureName);
+ chosenCaptureNames.add(captureName);
+ continue;
+ }
+
+ final String generated = nameGenerator.freshCaptureName(reservedNames, chosenCaptureNames);
reservedNames.add(generated);
+ chosenCaptureNames.add(generated);
renamedCaptures.put(captureName, generated);
}
@@ -85,18 +100,43 @@ public final class ValueEmitter {
final Set<String> bound = new HashSet<>();
bound.add(parameter);
- final Expression rewrittenBody = renameFreeIdentifiers(sanitizeExpression(body), renamedCaptures, bound);
+ final Expression rewrittenBody = renameFreeIdentifiers(sanitizedBody, renamedCaptures, bound);
return new ProgramFragment(macros, abstraction(parameter, rewrittenBody));
}
- private static LinkedHashMap<String, Supplier<Value>> captures(final Environment env) {
+ private static LinkedHashMap<String, Supplier<Value>> captures(final Environment env,
+ final Set<String> referencedNames) {
final LinkedHashMap<String, Supplier<Value>> captures = new LinkedHashMap<>();
for (final Environment.BindingFrame frame : env.bindingFrames()) {
+ if (!referencedNames.contains(frame.name())) {
+ continue;
+ }
captures.putIfAbsent(frame.name(), frame.value());
}
return captures;
}
+ private static Set<String> freeIdentifiers(final Expression expression, final Set<String> boundNames) {
+ return switch (expression) {
+ case IdentifierExpression(var _, var _, String name) -> {
+ if (boundNames.contains(name)) {
+ yield Set.of();
+ }
+ yield Set.of(name);
+ }
+ case ApplicationExpression(var _, var _, Expression applicable, Expression argument) -> {
+ final Set<String> free = new HashSet<>(freeIdentifiers(applicable, boundNames));
+ free.addAll(freeIdentifiers(argument, boundNames));
+ yield free;
+ }
+ case AbstractionExpression(var _, var _, String parameter, Expression body) -> {
+ final Set<String> nestedBound = new HashSet<>(boundNames);
+ nestedBound.add(parameter);
+ yield freeIdentifiers(body, nestedBound);
+ }
+ };
+ }
+
private static Expression renameFreeIdentifiers(final Expression expression, final Map<String, String> renameMap,
final Set<String> boundNames) {
return switch (expression) {
@@ -165,11 +205,11 @@ public final class ValueEmitter {
private static final class NameGenerator {
private int nextCapture = 0;
- private String freshCaptureName(final Set<String> reservedNames) {
+ private String freshCaptureName(final Set<String> reservedNames, final Set<String> chosenNames) {
String candidate;
do {
candidate = "_cap" + nextCapture++;
- } while (reservedNames.contains(candidate));
+ } while (reservedNames.contains(candidate) || chosenNames.contains(candidate));
return candidate;
}
}
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
index 6829417..b70f669 100644
--- a/core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java
+++ b/core/src/test/java/coffee/liz/abstractionengine/grid/system/LambdaEvaluatorSystemTest.java
@@ -15,6 +15,7 @@ import coffee.liz.lambda.ast.SourceCode;
import org.junit.jupiter.api.Test;
import java.time.Duration;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -23,7 +24,7 @@ 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 IDENTITY_FUNCTION = SourceCode.ofArrow("x -> x");
private static final SourceCode FREE = SourceCode.ofArrow("z");
@Test
@@ -31,8 +32,8 @@ public class LambdaEvaluatorSystemTest {
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(1).y(1).build())).add(new LambdaTerm(IDENTITY_FUNCTION))
+ .add(new Substitution(Vec2i.EAST, List.of(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);
@@ -50,8 +51,8 @@ public class LambdaEvaluatorSystemTest {
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(1).y(1).build())).add(new LambdaTerm(IDENTITY_FUNCTION))
+ .add(new Substitution(Vec2i.EAST, List.of(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);
@@ -68,8 +69,8 @@ public class LambdaEvaluatorSystemTest {
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(1).y(1).build())).add(new LambdaTerm(IDENTITY_FUNCTION))
+ .add(new Substitution(Vec2i.EAST, List.of(Vec2i.builder().x(0).y(0).build())));
world.update(GridInputState.builder().build(), Duration.ZERO);
@@ -79,12 +80,12 @@ public class LambdaEvaluatorSystemTest {
@Test
public void testSpawnsInSubstitutionDirection() {
- final Vec2<Integer> spawnMomentum = Vec2i.builder().x(0).y(-1).build();
+ final Vec2<Integer> spawnMomentum = Vec2i.SOUTH;
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(1).y(1).build())).add(new LambdaTerm(IDENTITY_FUNCTION))
+ .add(new Substitution(spawnMomentum, List.of(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);
@@ -94,4 +95,26 @@ public class LambdaEvaluatorSystemTest {
final GridMomentum momentum = emittedTerms.iterator().next().get(GridMomentum.class);
assertEquals(new GridMomentum(spawnMomentum), momentum);
}
+
+ @Test
+ public void testDependenciesAppliedInNumericOrder() {
+ final Vec2<Integer> firstDependencyPos = Vec2i.builder().x(0).y(0).build();
+ final Vec2<Integer> secondDependencyPos = Vec2i.builder().x(2).y(0).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(SourceCode.ofArrow("f -> g -> f")))
+ .add(new Substitution(Vec2i.EAST, List.of(firstDependencyPos, secondDependencyPos)));
+ world.createEntity().add(new GridPosition(firstDependencyPos)).add(new LambdaTerm(SourceCode.ofArrow("one")));
+ world.createEntity().add(new GridPosition(secondDependencyPos)).add(new LambdaTerm(SourceCode.ofArrow("two")));
+
+ 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.Arrow arrow = assertInstanceOf(SourceCode.Arrow.class, emitted);
+ assertEquals("one", arrow.source());
+ }
}
diff --git a/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java b/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java
index c1ab704..959f45d 100644
--- a/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java
+++ b/core/src/test/java/coffee/liz/lambda/format/ValueEmitterTest.java
@@ -12,7 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class ValueEmitterTest {
@Test
- void emitsFreeInBothSyntaxes() {
+ public void testEmitsFreeInBothSyntaxes() {
final Value value = LambdaDriver.interpret(SourceCode.ofArrow("z"));
final SourceCode arrow = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
@@ -23,7 +23,7 @@ class ValueEmitterTest {
}
@Test
- void emitsApplicationInBothSyntaxes() {
+ public void testEmitsApplicationInBothSyntaxes() {
final Value value = LambdaDriver.interpret(SourceCode.ofArrow("f(x)"));
final SourceCode arrow = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
@@ -34,27 +34,41 @@ class ValueEmitterTest {
}
@Test
- void emitsClosureWithCaptureMacros() {
+ public void testEmitsClosureWithCaptureMacros() {
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"));
+ assertEquals("""
+ let x = z;
+
+ y -> x""", source);
- // Validate emitted program is parsable and evaluates.
final Value roundTrip = LambdaDriver.interpret(emitted);
assertInstanceOf(Value.Closure.class, roundTrip);
}
@Test
- void emitsHygienicCapturesWhenNamesShadow() {
+ public void testEmitsHygienicCapturesWhenNamesShadow() {
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 ="));
+ assertEquals("""
+ let x = y;
+
+ y -> x""", source);
+ }
+
+ @Test
+ public void testPrunesUnusedEnvironmentCaptures() {
+ final Value value = LambdaDriver.interpret(SourceCode.ofArrow("(x -> w -> y -> x)(z)(q)"));
+ final SourceCode emitted = ValueEmitter.emit(value, SourceCode.Syntax.ARROW);
+ final String source = assertInstanceOf(SourceCode.Arrow.class, emitted).source();
+
+ assertEquals("""
+ let x = z;
+
+ y -> x""", source);
}
}