diff options
19 files changed, 645 insertions, 117 deletions
diff --git a/Grid.java b/Grid.java new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/Grid.java 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); } } |
