From 4d6e608f5992106fec55f81d6b243e91d62d5b6b Mon Sep 17 00:00:00 2001 From: Rob Greene Date: Sun, 10 Jun 2018 10:39:14 -0500 Subject: [PATCH] Optimization to move constant values into variables. Closes #4. --- .../bastokenizer/api/Optimization.java | 6 + .../bastokenizer/api/model/Line.java | 4 + .../optimizations/ExtractConstantValues.java | 149 ++++++++++++++++++ .../bastokenizer/tools/bt/Main.java | 1 + 4 files changed, 160 insertions(+) create mode 100644 api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/ExtractConstantValues.java diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java index 90a6c72..4bb15e5 100644 --- a/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/Optimization.java @@ -2,14 +2,20 @@ package io.github.applecommander.bastokenizer.api; import java.util.function.Function; +import io.github.applecommander.bastokenizer.api.optimizations.ExtractConstantValues; import io.github.applecommander.bastokenizer.api.optimizations.MergeLines; import io.github.applecommander.bastokenizer.api.optimizations.RemoveEmptyStatements; import io.github.applecommander.bastokenizer.api.optimizations.RemoveRemStatements; import io.github.applecommander.bastokenizer.api.optimizations.Renumber; +/** + * All optimization capabilities are definined here in the "best" manner of execution. + * Essentially, the goal is to prioritize the optimizations to manage dependencies. + */ public enum Optimization { REMOVE_EMPTY_STATEMENTS(RemoveEmptyStatements::new), REMOVE_REM_STATEMENTS(RemoveRemStatements::new), + EXTRACT_CONSTANT_VALUES(ExtractConstantValues::new), MERGE_LINES(MergeLines::new), RENUMBER(Renumber::new); diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java index b6346d2..8f03b15 100644 --- a/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/model/Line.java @@ -19,6 +19,10 @@ public class Line { this.program = program; } + public int getLineNumber() { + return lineNumber; + } + public Optional nextLine() { int i = program.lines.indexOf(this); if (i == -1 || i+1 >= program.lines.size()) { diff --git a/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/ExtractConstantValues.java b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/ExtractConstantValues.java new file mode 100644 index 0000000..fd77545 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastokenizer/api/optimizations/ExtractConstantValues.java @@ -0,0 +1,149 @@ +package io.github.applecommander.bastokenizer.api.optimizations; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import io.github.applecommander.bastokenizer.api.Configuration; +import io.github.applecommander.bastokenizer.api.Visitors; +import io.github.applecommander.bastokenizer.api.model.ApplesoftKeyword; +import io.github.applecommander.bastokenizer.api.model.Line; +import io.github.applecommander.bastokenizer.api.model.Program; +import io.github.applecommander.bastokenizer.api.model.Statement; +import io.github.applecommander.bastokenizer.api.model.Token; +import io.github.applecommander.bastokenizer.api.utils.VariableNameGenerator; +import io.github.applecommander.bastokenizer.api.visitors.VariableCollectorVisitor; + +/** + * Find constants and extract to variables in order to have the number parsed only once. + */ +public class ExtractConstantValues extends BaseVisitor { + /** These trigger the start of a replacement range. Note the special logic for assignments. */ + public static List TARGET_STARTS = Arrays.asList( + ApplesoftKeyword.FOR, ApplesoftKeyword.CALL, ApplesoftKeyword.PLOT, ApplesoftKeyword.HLIN, + ApplesoftKeyword.VLIN, ApplesoftKeyword.HCOLOR, ApplesoftKeyword.HPLOT, ApplesoftKeyword.DRAW, + ApplesoftKeyword.XDRAW, ApplesoftKeyword.HTAB, ApplesoftKeyword.SCALE, ApplesoftKeyword.COLOR, + ApplesoftKeyword.VTAB, ApplesoftKeyword.HIMEM, ApplesoftKeyword.LOMEM, ApplesoftKeyword.SPEED, + ApplesoftKeyword.LET, ApplesoftKeyword.IF, ApplesoftKeyword.ON, ApplesoftKeyword.WAIT, + ApplesoftKeyword.POKE); + /** These trigger the end of a replacement range. End of statement is always an end. */ + public static List TARGET_ENDS = Arrays.asList( + ApplesoftKeyword.GOTO, ApplesoftKeyword.GOSUB, ApplesoftKeyword.THEN); + + // Map keyed by value (Double isn't a good key, using a String of the number) and pointing to replacement variable name + private Map map = new HashMap<>(); + + private VariableNameGenerator variableGenerator = new VariableNameGenerator(); + private Set existingVariables; + private Function consumer = this::nullTransformation; + + public ExtractConstantValues(Configuration config) { + // ignored + } + + public Token nullTransformation(Token token) { + return token; + } + /** Collect a map of constant values and the new variable name to be used. */ + public Token numberToIdentTransformation(Token token) { + String key = token.number.toString(); + // New entry, create it + if (!map.containsKey(key)) { + String varName = null; + do { + varName = variableGenerator.get() + .orElseThrow(() -> new RuntimeException("Ran out of variable names to assign")); + } while (existingVariables.contains(varName)); + map.put(key, varName); + } + // Existing (or NEW!) entry, swap to that variable. + if (map.containsKey(key)) { + return Token.ident(token.line, map.get(key)); + } + return token; + } + + @Override + public Program visit(Program program) { + VariableCollectorVisitor collector = Visitors.variableCollectorVisitor(); + program.accept(collector); + this.existingVariables = collector.getVariableNames(); + + program = super.visit(program); + + injectLine0(program); + + return program; + } + private void injectLine0(Program program) { + Line line = generateLine0(program); + // setup a renumber of lines that interfere if we have any + if (program.lines.get(0).lineNumber == 0) { + // start with line #0 should become line #1 + super.reassignments.put(0, 1); + // chase it to the end! + program.lines.stream() + .map(Line::getLineNumber) + .filter(super.reassignments::containsValue) + .forEach(n -> { super.reassignments.put(n, n+1); }); + } + program.lines.add(0, line); + } + private Line generateLine0(Program program) { + Line line = new Line(0, program); + map.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .map(this::toStatement) + .forEach(line.statements::add); + return line; + } + private Statement toStatement(Map.Entry variable) { + Statement statement = new Statement(); + statement.tokens.add(Token.ident(-1, variable.getValue())); + statement.tokens.add(Token.syntax(-1, '=')); + statement.tokens.add(Token.number(-1, Double.valueOf(variable.getKey()))); + return statement; + } + + @Override + public Statement visit(Statement statement) { + try { + if (!statement.tokens.isEmpty()) { + int size = statement.tokens.size(); + Token t = statement.tokens.get(0); + // Special logic for "A=5+1" while trying to skip constant forms of "A=1234" (don't replicate) + if (t.type == Token.Type.IDENT && size > 3) { + this.consumer = this::numberToIdentTransformation; + } + // Special logic for "LET A=5+1" while trying to skip constant forms of "LET A=1234" (don't replicate) + if (t.type == Token.Type.KEYWORD && t.keyword == ApplesoftKeyword.LET && size > 4) { + this.consumer = this::numberToIdentTransformation; + } + } + return super.visit(statement); + } finally { + this.consumer = this::nullTransformation; + } + } + + @Override + public Token visit(Token token) { + switch (token.type) { + case KEYWORD: + if (TARGET_STARTS.contains(token.keyword)) { + this.consumer = this::numberToIdentTransformation; + } else if (TARGET_ENDS.contains(token.keyword)) { + this.consumer = this::nullTransformation; + } + break; + case NUMBER: + return this.consumer.apply(token); + default: + break; + } + return super.visit(token); + } +} diff --git a/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java index 2e53c48..4f1e0b3 100644 --- a/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java +++ b/tools/bt/src/main/java/io/github/applecommander/bastokenizer/tools/bt/Main.java @@ -78,6 +78,7 @@ public class Main implements Callable { "Enable specific optimizations.", "* @|green remove-empty-statements|@ - Strip out all '::'-like statements.", "* @|green remove-rem-statements|@ - Remove all REM statements.", + "* @|green extract-constant-values|@ - Assign all constant values first.", "* @|green merge-lines|@ - Merge lines.", "* @|green renumber|@ - Renumber program." })