From b2924164967767ca8c18e0e0094fed81ca9b2eff Mon Sep 17 00:00:00 2001 From: NotMyFault Date: Sat, 4 Jan 2020 18:34:30 +0100 Subject: [PATCH] Upstream and some refactoring Note: Maybe this doesn't compile, ij is broken af smh, let's give it a try... --- .../sk89q/worldedit/bukkit/BukkitAdapter.java | 27 + .../bukkit/BukkitBlockCommandSender.java | 4 +- .../worldedit/bukkit/BukkitCommandSender.java | 4 + .../minecraft/util/commands/package-info.java | 1 + .../worldedit/command/HistoryCommands.java | 2 +- .../worldedit/command/tool/QueryTool.java | 12 +- .../internal/expression/BreakException.java | 5 +- .../expression/CompiledExpression.java | 29 + .../expression/EvaluatingVisitor.java | 625 ----------------- .../expression/EvaluationException.java | 2 - .../internal/expression/ExecutionData.java | 51 ++ .../internal/expression/Expression.java | 31 +- .../internal/expression/ExpressionHelper.java | 71 +- .../internal/expression/Functions.java | 33 +- .../expression/invoke/CompilingVisitor.java | 653 ++++++++++++++++++ .../internal/expression/invoke/ExecNode.java | 34 + .../expression/invoke/ExpressionCompiler.java | 73 ++ .../expression/invoke/ExpressionHandles.java | 339 +++++++++ .../worldedit/world/block/BlockType.java | 46 +- .../sk89q/worldedit/world/item/ItemType.java | 15 + .../world/registry/BundledItemData.java | 17 + .../world/registry/BundledItemRegistry.java | 14 +- .../world/registry/ItemMaterial.java | 36 + .../world/registry/ItemRegistry.java | 9 + .../registry/PassthroughBlockMaterial.java | 138 +--- .../registry/PassthroughItemMaterial.java | 45 ++ .../world/registry/SimpleMaterial.java | 41 ++ .../src/main/resources/lang/strings.json | 3 +- .../internal/expression/ExpressionTest.java | 1 - 29 files changed, 1556 insertions(+), 805 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/CompiledExpression.java delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExecutionData.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExecNode.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemMaterial.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughItemMaterial.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/SimpleMaterial.java diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitAdapter.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitAdapter.java index 1aa189540..dc2a46c9e 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitAdapter.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitAdapter.java @@ -28,6 +28,7 @@ import com.sk89q.worldedit.bukkit.adapter.IBukkitAdapter; import com.sk89q.worldedit.bukkit.adapter.SimpleBukkitAdapter; import com.sk89q.worldedit.entity.Entity; import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extension.platform.PlayerProxy; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.math.Vector3; @@ -43,6 +44,7 @@ import com.sk89q.worldedit.world.item.ItemType; import org.bukkit.Material; import org.bukkit.block.Biome; import org.bukkit.block.data.BlockData; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -108,6 +110,16 @@ public enum BukkitAdapter { return getAdapter().adapt(world); } + /** + * Create a WorldEdit Actor from a Bukkit CommandSender + * + * @param sender The Bukkit CommandSender + * @return The WorldEdit Actor + */ + public static Actor adapt(CommandSender sender) { + return WorldEditPlugin.getInstance().wrapCommandSender(sender); + } + /** * Create a WorldEdit Player from a Bukkit Player. * @@ -118,6 +130,21 @@ public enum BukkitAdapter { return WorldEditPlugin.getInstance().wrapPlayer(player); } + /** + * Create a Bukkit CommandSender from a WorldEdit Actor. + * + * @param actor The WorldEdit actor + * @return The Bukkit command sender + */ + public static CommandSender adapt(Actor actor) { + if (actor instanceof com.sk89q.worldedit.entity.Player) { + return adapt((com.sk89q.worldedit.entity.Player) actor); + } else if (actor instanceof BukkitBlockCommandSender) { + return ((BukkitBlockCommandSender) actor).getSender(); + } + return ((BukkitCommandSender) actor).getSender(); + } + /** * Create a Bukkit Player from a WorldEdit Player. * diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java index 3c293f6f3..92fc54fd2 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitBlockCommandSender.java @@ -34,7 +34,6 @@ import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.command.BlockCommandSender; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.nio.charset.StandardCharsets; import java.util.Locale; @@ -127,7 +126,10 @@ public class BukkitBlockCommandSender extends AbstractNonPlayerActor implements @Override public void setPermission(String permission, boolean value) { + } + public BlockCommandSender getSender() { + return this.sender; } @Override diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitCommandSender.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitCommandSender.java index 3695a9d82..8d8303b7c 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitCommandSender.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitCommandSender.java @@ -101,6 +101,10 @@ public class BukkitCommandSender extends AbstractNonPlayerActor { return WorldEdit.getInstance().getConfiguration().defaultLocale; } + public CommandSender getSender() { + return this.sender; + } + @Override public SessionKey getSessionKey() { return new SessionKey() { diff --git a/worldedit-core/src/main/java/com/sk89q/minecraft/util/commands/package-info.java b/worldedit-core/src/main/java/com/sk89q/minecraft/util/commands/package-info.java index 8dd9c94ff..3312b9241 100644 --- a/worldedit-core/src/main/java/com/sk89q/minecraft/util/commands/package-info.java +++ b/worldedit-core/src/main/java/com/sk89q/minecraft/util/commands/package-info.java @@ -21,4 +21,5 @@ * This package contains the old command system. It is no longer in use. Please switch * to Piston, Intake, ACF, or similar systems. */ +@Deprecated package com.sk89q.minecraft.util.commands; \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistoryCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistoryCommands.java index 2c2f46756..c95f6bcd5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistoryCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistoryCommands.java @@ -131,7 +131,7 @@ public class HistoryCommands { } } if (timesRedone > 0) { - actor.printInfo(TranslatableComponent.of("worldedit.redo.undone", TextComponent.of(timesRedone))); + actor.printInfo(TranslatableComponent.of("worldedit.redo.redone", TextComponent.of(timesRedone))); } else { actor.printError(TranslatableComponent.of("worldedit.redo.none")); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/QueryTool.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/QueryTool.java index 3ceee3a4b..b05c0376b 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/QueryTool.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/tool/QueryTool.java @@ -24,8 +24,8 @@ import com.sk89q.worldedit.LocalConfiguration; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.extension.platform.Actor; -import com.sk89q.worldedit.internal.block.BlockStateIdAccess; import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.internal.block.BlockStateIdAccess; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.util.Location; import com.sk89q.worldedit.util.formatting.text.TextComponent; @@ -34,6 +34,7 @@ import com.sk89q.worldedit.util.formatting.text.event.HoverEvent; import com.sk89q.worldedit.util.formatting.text.format.TextColor; import com.sk89q.worldedit.world.World; import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.registry.LegacyMapper; import java.util.OptionalInt; @@ -63,10 +64,15 @@ public class QueryTool implements BlockTool { final OptionalInt internalId = BlockStateIdAccess.getBlockStateId(block.toImmutableState()); if (internalId.isPresent()) { builder.append(TextComponent.of(" (" + internalId.getAsInt() + ") ", TextColor.DARK_GRAY) - .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TranslatableComponent.of("worldedit.tool.info.internalid.hover")))); + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TranslatableComponent.of("worldedit.tool.info.internalid.hover")))); + } + final int[] legacy = LegacyMapper.getInstance().getLegacyFromBlock(block.toImmutableState()); + if (legacy != null) { + builder.append(TextComponent.of(" (" + legacy[0] + ":" + legacy[1] + ") ", TextColor.DARK_GRAY) + .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TranslatableComponent.of("worldedit.tool.info.legacy.hover")))); } builder.append(TextComponent.of(" (" + world.getBlockLightLevel(blockPoint) + "/" - + world.getBlockLightLevel(blockPoint.add(0, 1, 0)) + ")", TextColor.WHITE) + + world.getBlockLightLevel(blockPoint.add(0, 1, 0)) + ")", TextColor.WHITE) .hoverEvent(HoverEvent.of(HoverEvent.Action.SHOW_TEXT, TranslatableComponent.of("worldedit.tool.info.light.hover")))); player.print(builder.build()); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java index 635c6d6ea..5b2ae6b24 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/BreakException.java @@ -25,9 +25,12 @@ package com.sk89q.worldedit.internal.expression; */ public class BreakException extends RuntimeException { + public static final BreakException BREAK = new BreakException(false); + public static final BreakException CONTINUE = new BreakException(true); + public final boolean doContinue; - public BreakException(boolean doContinue) { + private BreakException(boolean doContinue) { super(doContinue ? "'continue' encountered outside a loop" : "'break' encountered outside a loop", null, true, false); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/CompiledExpression.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/CompiledExpression.java new file mode 100644 index 000000000..b1f3577f6 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/CompiledExpression.java @@ -0,0 +1,29 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +/** + * Represents a "compiled" expression. + */ +public interface CompiledExpression { + + Double execute(ExecutionData executionData); + +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java deleted file mode 100644 index bd282e9e1..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluatingVisitor.java +++ /dev/null @@ -1,625 +0,0 @@ -/* - * WorldEdit, a Minecraft world manipulation toolkit - * Copyright (C) sk89q - * Copyright (C) WorldEdit team and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.internal.expression; - -import com.google.common.base.Throwables; -import com.google.common.collect.SetMultimap; -import com.sk89q.worldedit.antlr.ExpressionBaseVisitor; -import com.sk89q.worldedit.antlr.ExpressionParser; -import it.unimi.dsi.fastutil.doubles.Double2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; -import it.unimi.dsi.fastutil.doubles.Double2ObjectMaps; -import org.antlr.v4.runtime.ParserRuleContext; -import org.antlr.v4.runtime.Token; -import org.antlr.v4.runtime.tree.ParseTree; -import org.antlr.v4.runtime.tree.RuleNode; -import org.antlr.v4.runtime.tree.TerminalNode; - -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodType; -import java.util.List; -import java.util.Optional; -import java.util.function.DoubleBinaryOperator; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import static com.sk89q.worldedit.antlr.ExpressionLexer.ASSIGN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.DIVIDE; -import static com.sk89q.worldedit.antlr.ExpressionLexer.DIVIDE_ASSIGN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.EQUAL; -import static com.sk89q.worldedit.antlr.ExpressionLexer.EXCLAMATION_MARK; -import static com.sk89q.worldedit.antlr.ExpressionLexer.GREATER_THAN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.GREATER_THAN_OR_EQUAL; -import static com.sk89q.worldedit.antlr.ExpressionLexer.INCREMENT; -import static com.sk89q.worldedit.antlr.ExpressionLexer.LEFT_SHIFT; -import static com.sk89q.worldedit.antlr.ExpressionLexer.LESS_THAN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.LESS_THAN_OR_EQUAL; -import static com.sk89q.worldedit.antlr.ExpressionLexer.MINUS; -import static com.sk89q.worldedit.antlr.ExpressionLexer.MINUS_ASSIGN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.MODULO; -import static com.sk89q.worldedit.antlr.ExpressionLexer.MODULO_ASSIGN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.NEAR; -import static com.sk89q.worldedit.antlr.ExpressionLexer.NOT_EQUAL; -import static com.sk89q.worldedit.antlr.ExpressionLexer.PLUS; -import static com.sk89q.worldedit.antlr.ExpressionLexer.PLUS_ASSIGN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.POWER_ASSIGN; -import static com.sk89q.worldedit.antlr.ExpressionLexer.RIGHT_SHIFT; -import static com.sk89q.worldedit.antlr.ExpressionLexer.TIMES; -import static com.sk89q.worldedit.antlr.ExpressionLexer.TIMES_ASSIGN; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.WRAPPED_CONSTANT; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.check; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.checkIterations; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.checkTimeout; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.evalException; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.getArgumentHandleName; -import static com.sk89q.worldedit.internal.expression.ExpressionHelper.resolveFunction; - -class EvaluatingVisitor extends ExpressionBaseVisitor { - - private final SlotTable slots; - private final SetMultimap functions; - - EvaluatingVisitor(SlotTable slots, - SetMultimap functions) { - this.slots = slots; - this.functions = functions; - } - - private LocalSlot.Variable initVariable(String name, ParserRuleContext ctx) { - return slots.initVariable(name) - .orElseThrow(() -> evalException(ctx, "Cannot overwrite non-variable '" + name + "'")); - } - - private Supplier varNotInitException(String name, ParserRuleContext ctx) { - return () -> evalException(ctx, "'" + name + "' is not initialized yet"); - } - - private LocalSlot.Variable getVariable(String name, ParserRuleContext ctx) { - LocalSlot slot = slots.getSlot(name) - .orElseThrow(varNotInitException(name, ctx)); - check(slot instanceof LocalSlot.Variable, ctx, "'" + name + "' is not a variable"); - return (LocalSlot.Variable) slot; - } - - private double getSlotValue(String name, ParserRuleContext ctx) { - return slots.getSlotValue(name) - .orElseThrow(varNotInitException(name, ctx)); - } - - private Token extractToken(ParserRuleContext ctx) { - List children = ctx.children.stream() - .filter(TerminalNode.class::isInstance) - .map(TerminalNode.class::cast) - .collect(Collectors.toList()); - check(children.size() == 1, ctx, "Expected exactly one token, got " + children.size()); - return children.get(0).getSymbol(); - } - - private Double evaluate(ParserRuleContext ctx) { - return ctx.accept(this); - } - - private double evaluateForValue(ParserRuleContext ctx) { - Double result = evaluate(ctx); - check(result != null, ctx, "Invalid expression for a value"); - return result; - } - - private boolean evaluateBoolean(ParserRuleContext boolExpression) { - Double bool = evaluate(boolExpression); - check(bool != null, boolExpression, "Invalid expression for boolean"); - return doubleToBool(bool); - } - - private boolean doubleToBool(double bool) { - return bool > 0; - } - - private double boolToDouble(boolean bool) { - return bool ? 1 : 0; - } - - private Double evaluateConditional(ParserRuleContext condition, - ParserRuleContext trueBranch, - ParserRuleContext falseBranch) { - ParserRuleContext ctx = evaluateBoolean(condition) ? trueBranch : falseBranch; - return ctx == null ? null : evaluate(ctx); - } - - @Override - public Double visitIfStatement(ExpressionParser.IfStatementContext ctx) { - return evaluateConditional(ctx.condition, ctx.trueBranch, ctx.falseBranch); - } - - @Override - public Double visitTernaryExpr(ExpressionParser.TernaryExprContext ctx) { - return evaluateConditional(ctx.condition, ctx.trueBranch, ctx.falseBranch); - } - - @Override - public Double visitWhileStatement(ExpressionParser.WhileStatementContext ctx) { - Double result = defaultResult(); - int iterations = 0; - while (evaluateBoolean(ctx.condition)) { - checkIterations(iterations, ctx.body); - checkTimeout(); - iterations++; - try { - result = evaluate(ctx.body); - } catch (BreakException ex) { - if (!ex.doContinue) { - break; - } - } - } - return result; - } - - @Override - public Double visitDoStatement(ExpressionParser.DoStatementContext ctx) { - Double result = defaultResult(); - int iterations = 0; - do { - checkIterations(iterations, ctx.body); - checkTimeout(); - iterations++; - try { - result = evaluate(ctx.body); - } catch (BreakException ex) { - if (!ex.doContinue) { - break; - } - } - } while (evaluateBoolean(ctx.condition)); - return result; - } - - @Override - public Double visitForStatement(ExpressionParser.ForStatementContext ctx) { - Double result = defaultResult(); - int iterations = 0; - evaluate(ctx.init); - while (evaluateBoolean(ctx.condition)) { - checkIterations(iterations, ctx.body); - checkTimeout(); - iterations++; - try { - result = evaluate(ctx.body); - } catch (BreakException ex) { - if (!ex.doContinue) { - break; - } - } - evaluate(ctx.update); - } - return result; - } - - @Override - public Double visitSimpleForStatement(ExpressionParser.SimpleForStatementContext ctx) { - Double result = defaultResult(); - int iterations = 0; - double first = evaluateForValue(ctx.first); - double last = evaluateForValue(ctx.last); - String counter = ctx.counter.getText(); - LocalSlot.Variable variable = initVariable(counter, ctx); - for (double i = first; i <= last; i++) { - checkIterations(iterations, ctx.body); - checkTimeout(); - iterations++; - variable.setValue(i); - try { - result = evaluate(ctx.body); - } catch (BreakException ex) { - if (!ex.doContinue) { - break; - } - } - } - return result; - } - - @Override - public Double visitBreakStatement(ExpressionParser.BreakStatementContext ctx) { - throw new BreakException(false); - } - - @Override - public Double visitContinueStatement(ExpressionParser.ContinueStatementContext ctx) { - throw new BreakException(true); - } - - @Override - public Double visitReturnStatement(ExpressionParser.ReturnStatementContext ctx) { - if (ctx.value != null) { - return evaluate(ctx.value); - } - return null; - } - - @Override - public Double visitSwitchStatement(ExpressionParser.SwitchStatementContext ctx) { - Double2ObjectMap cases = new Double2ObjectLinkedOpenHashMap<>(ctx.labels.size()); - ParserRuleContext defaultCase = null; - for (int i = 0; i < ctx.labels.size(); i++) { - ExpressionParser.SwitchLabelContext label = ctx.labels.get(i); - ExpressionParser.StatementsContext body = ctx.bodies.get(i); - if (label instanceof ExpressionParser.CaseContext) { - ExpressionParser.CaseContext caseContext = (ExpressionParser.CaseContext) label; - double key = evaluateForValue(caseContext.constant); - check(!cases.containsKey(key), body, "Duplicate cases detected."); - cases.put(key, body); - } else { - check(defaultCase == null, body, "Duplicate default cases detected."); - defaultCase = body; - } - } - double value = evaluateForValue(ctx.target); - boolean matched = false; - Double evaluated = null; - boolean falling = false; - for (Double2ObjectMap.Entry entry : Double2ObjectMaps.fastIterable(cases)) { - if (falling || entry.getDoubleKey() == value) { - matched = true; - try { - evaluated = evaluate(entry.getValue()); - falling = true; - } catch (BreakException brk) { - check(!brk.doContinue, entry.getValue(), "Cannot continue in a switch"); - falling = false; - break; - } - } - } - // This if is like the one in the loop, default's "case" is `!matched` & present - if ((falling || !matched) && defaultCase != null) { - try { - evaluated = evaluate(defaultCase); - } catch (BreakException brk) { - check(!brk.doContinue, defaultCase, "Cannot continue in a switch"); - } - } - return evaluated; - } - - @Override - public Double visitExpressionStatement(ExpressionParser.ExpressionStatementContext ctx) { - return evaluate(ctx.expression()); - } - - @Override - public Double visitPostCrementExpr(ExpressionParser.PostCrementExprContext ctx) { - String target = ctx.target.getText(); - LocalSlot.Variable variable = getVariable(target, ctx); - double value = variable.getValue(); - if (ctx.op.getType() == INCREMENT) { - value++; - } else { - value--; - } - variable.setValue(value); - return value; - } - - @Override - public Double visitPreCrementExpr(ExpressionParser.PreCrementExprContext ctx) { - String target = ctx.target.getText(); - LocalSlot.Variable variable = getVariable(target, ctx); - double value = variable.getValue(); - double result = value; - if (ctx.op.getType() == INCREMENT) { - value++; - } else { - value--; - } - variable.setValue(value); - return result; - } - - @Override - public Double visitPlusMinusExpr(ExpressionParser.PlusMinusExprContext ctx) { - double value = evaluateForValue(ctx.expr); - switch (ctx.op.getType()) { - case PLUS: - return +value; - case MINUS: - return -value; - } - throw evalException(ctx, "Invalid text for plus/minus expr: " + ctx.op.getText()); - } - - @Override - public Double visitNotExpr(ExpressionParser.NotExprContext ctx) { - return boolToDouble(!evaluateBoolean(ctx.expr)); - } - - @Override - public Double visitComplementExpr(ExpressionParser.ComplementExprContext ctx) { - return (double) ~(long) evaluateForValue(ctx.expr); - } - - @Override - public Double visitConditionalAndExpr(ExpressionParser.ConditionalAndExprContext ctx) { - if (!evaluateBoolean(ctx.left)) { - return boolToDouble(false); - } - return evaluateForValue(ctx.right); - } - - @Override - public Double visitConditionalOrExpr(ExpressionParser.ConditionalOrExprContext ctx) { - double left = evaluateForValue(ctx.left); - if (doubleToBool(left)) { - return left; - } - return evaluateForValue(ctx.right); - } - - private double evaluateBinary(ParserRuleContext left, - ParserRuleContext right, - DoubleBinaryOperator op) { - return op.applyAsDouble(evaluateForValue(left), evaluateForValue(right)); - } - - @Override - public Double visitPowerExpr(ExpressionParser.PowerExprContext ctx) { - return evaluateBinary(ctx.left, ctx.right, Math::pow); - } - - @Override - public Double visitMultiplicativeExpr(ExpressionParser.MultiplicativeExprContext ctx) { - return evaluateBinary(ctx.left, ctx.right, (l, r) -> { - switch (ctx.op.getType()) { - case TIMES: - return l * r; - case DIVIDE: - return l / r; - case MODULO: - return l % r; - } - throw evalException(ctx, "Invalid text for multiplicative expr: " + ctx.op.getText()); - }); - } - - @Override - public Double visitAddExpr(ExpressionParser.AddExprContext ctx) { - return evaluateBinary(ctx.left, ctx.right, (l, r) -> { - switch (ctx.op.getType()) { - case PLUS: - return l + r; - case MINUS: - return l - r; - } - throw evalException(ctx, "Invalid text for additive expr: " + ctx.op.getText()); - }); - } - - @Override - public Double visitShiftExpr(ExpressionParser.ShiftExprContext ctx) { - return evaluateBinary(ctx.left, ctx.right, (l, r) -> { - switch (ctx.op.getType()) { - case LEFT_SHIFT: - return (double) ((long) l << (long) r); - case RIGHT_SHIFT: - return (double) ((long) l >> (long) r); - } - throw evalException(ctx, "Invalid text for shift expr: " + ctx.op.getText()); - }); - } - - @Override - public Double visitRelationalExpr(ExpressionParser.RelationalExprContext ctx) { - return evaluateBinary(ctx.left, ctx.right, (l, r) -> { - switch (ctx.op.getType()) { - case LESS_THAN: - return boolToDouble(l < r); - case LESS_THAN_OR_EQUAL: - return boolToDouble(l <= r); - case GREATER_THAN: - return boolToDouble(l > r); - case GREATER_THAN_OR_EQUAL: - return boolToDouble(l >= r); - } - throw evalException(ctx, "Invalid text for relational expr: " + ctx.op.getText()); - }); - } - - @Override - public Double visitEqualityExpr(ExpressionParser.EqualityExprContext ctx) { - return evaluateBinary(ctx.left, ctx.right, (l, r) -> { - switch (ctx.op.getType()) { - case EQUAL: - return boolToDouble(l == r); - case NOT_EQUAL: - return boolToDouble(l != r); - case NEAR: - return boolToDouble(almostEqual2sComplement(l, r, 450359963L)); - case GREATER_THAN_OR_EQUAL: - return boolToDouble(l >= r); - } - throw evalException(ctx, "Invalid text for equality expr: " + ctx.op.getText()); - }); - } - - // Usable AlmostEqual function, based on http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm - private static boolean almostEqual2sComplement(double a, double b, long maxUlps) { - // Make sure maxUlps is non-negative and small enough that the - // default NAN won't compare as equal to anything. - //assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024); // this is for floats, not doubles - - long aLong = Double.doubleToRawLongBits(a); - // Make aLong lexicographically ordered as a twos-complement long - if (aLong < 0) aLong = 0x8000000000000000L - aLong; - - long bLong = Double.doubleToRawLongBits(b); - // Make bLong lexicographically ordered as a twos-complement long - if (bLong < 0) bLong = 0x8000000000000000L - bLong; - - final long longDiff = Math.abs(aLong - bLong); - return longDiff <= maxUlps; - } - - @Override - public Double visitPostfixExpr(ExpressionParser.PostfixExprContext ctx) { - double value = evaluateForValue(ctx.expr); - if (ctx.op.getType() == EXCLAMATION_MARK) { - return factorial(value); - } - throw evalException(ctx, - "Invalid text for post-unary expr: " + ctx.op.getText()); - } - - private static final double[] factorials = new double[171]; - - static { - factorials[0] = 1; - for (int i = 1; i < factorials.length; ++i) { - factorials[i] = factorials[i - 1] * i; - } - } - - private static double factorial(double x) throws EvaluationException { - final int n = (int) x; - - if (n < 0) { - return 0; - } - - if (n >= factorials.length) { - return Double.POSITIVE_INFINITY; - } - - return factorials[n]; - } - - @Override - public Double visitAssignment(ExpressionParser.AssignmentContext ctx) { - int type = extractToken(ctx.assignmentOperator()).getType(); - String target = ctx.target.getText(); - double value; - double arg = evaluateForValue(ctx.expression()); - LocalSlot.Variable variable; - if (type == ASSIGN) { - variable = initVariable(target, ctx); - value = arg; - } else { - variable = getVariable(target, ctx); - value = variable.getValue(); - switch (type) { - case POWER_ASSIGN: - value = Math.pow(value, arg); - break; - case TIMES_ASSIGN: - value *= arg; - break; - case DIVIDE_ASSIGN: - value /= arg; - break; - case MODULO_ASSIGN: - value %= arg; - break; - case PLUS_ASSIGN: - value += arg; - break; - case MINUS_ASSIGN: - value -= arg; - break; - default: - throw evalException(ctx, "Invalid text for assign expr: " + - ctx.assignmentOperator().getText()); - } - } - variable.setValue(value); - return value; - } - - @Override - public Double visitFunctionCall(ExpressionParser.FunctionCallContext ctx) { - MethodHandle handle = resolveFunction(functions, ctx); - String fnName = ctx.name.getText(); - Object[] arguments = new Object[ctx.args.size()]; - for (int i = 0; i < arguments.length; i++) { - ExpressionParser.ExpressionContext arg = ctx.args.get(i); - Object transformed = getArgument(fnName, handle.type(), i, arg); - arguments[i] = transformed; - } - try { - // Some methods return other Numbers - Number number = (Number) handle.invokeWithArguments(arguments); - return number == null ? null : number.doubleValue(); - } catch (Throwable throwable) { - Throwables.throwIfUnchecked(throwable); - throw new RuntimeException(throwable); - } - } - - private Object getArgument(String fnName, MethodType type, int i, ParserRuleContext arg) { - // Pass variable handle in for modification? - String handleName = getArgumentHandleName(fnName, type, i, arg); - if (handleName == null) { - return evaluateForValue(arg); - } - if (handleName.equals(WRAPPED_CONSTANT)) { - return new LocalSlot.Constant(evaluateForValue(arg)); - } - return getVariable(handleName, arg); - } - - @Override - public Double visitConstantExpression(ExpressionParser.ConstantExpressionContext ctx) { - try { - return Double.parseDouble(ctx.getText()); - } catch (NumberFormatException e) { - // Rare, but might happen, e.g. if too many digits - throw evalException(ctx, "Invalid constant: " + e.getMessage()); - } - } - - @Override - public Double visitIdExpr(ExpressionParser.IdExprContext ctx) { - String source = ctx.source.getText(); - return getSlotValue(source, ctx); - } - - @Override - public Double visitChildren(RuleNode node) { - Double result = defaultResult(); - int n = node.getChildCount(); - for (int i = 0; i < n; i++) { - ParseTree c = node.getChild(i); - if (c instanceof TerminalNode && ((TerminalNode) c).getSymbol().getType() == Token.EOF) { - break; - } - - Double childResult = c.accept(this); - if (c instanceof ExpressionParser.ReturnStatementContext) { - return childResult; - } - result = aggregateResult(result, childResult); - } - - return result; - } - - @Override - protected Double aggregateResult(Double aggregate, Double nextResult) { - return Optional.ofNullable(nextResult).orElse(aggregate); - } -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java index c9042d7ca..5db81ef38 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/EvaluationException.java @@ -19,8 +19,6 @@ package com.sk89q.worldedit.internal.expression; -import com.sk89q.worldedit.internal.expression.ExpressionException; - /** * Thrown when there's a problem during expression evaluation. */ diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExecutionData.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExecutionData.java new file mode 100644 index 000000000..b29cb1ef9 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExecutionData.java @@ -0,0 +1,51 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression; + +import com.google.common.collect.SetMultimap; + +import java.lang.invoke.MethodHandle; + +import static java.util.Objects.requireNonNull; + +public class ExecutionData { + + /** + * Special execution context for evaluating constant values. As long as no variables are used, + * it can be considered constant. + */ + public static final ExecutionData CONSTANT_EVALUATOR = new ExecutionData(null, null); + + private final SlotTable slots; + private final SetMultimap functions; + + public ExecutionData(SlotTable slots, SetMultimap functions) { + this.slots = slots; + this.functions = functions; + } + + public SlotTable getSlots() { + return requireNonNull(slots, "Cannot use variables in a constant"); + } + + public SetMultimap getFunctions() { + return requireNonNull(functions, "Cannot use functions in a constant"); + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java index 17efef064..e50230086 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Expression.java @@ -26,6 +26,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.antlr.ExpressionLexer; import com.sk89q.worldedit.antlr.ExpressionParser; +import com.sk89q.worldedit.internal.expression.invoke.ExpressionCompiler; import com.sk89q.worldedit.session.request.Request; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; @@ -37,13 +38,7 @@ import java.lang.invoke.MethodHandle; import java.util.List; import java.util.Objects; import java.util.Stack; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.*; /** * Compiles and evaluates expressions. @@ -83,14 +78,11 @@ public class Expression { private final SlotTable slots = new SlotTable(); private final List providedSlots; - private ExpressionParser.AllStatementsContext root; + private final ExpressionParser.AllStatementsContext root; private final SetMultimap functions = Functions.getFunctionMap(); + private final CompiledExpression compiledExpression; private ExpressionEnvironment environment; - public static Expression compile(String expression, String... variableNames) throws ExpressionException { - return new Expression(expression, variableNames); - } - private Expression(String expression, String... variableNames) throws ExpressionException { slots.putSlot("e", new LocalSlot.Constant(Math.E)); slots.putSlot("pi", new LocalSlot.Constant(Math.PI)); @@ -119,6 +111,15 @@ public class Expression { throw new ParserException(parser.getState(), e); } ParseTreeWalker.DEFAULT.walk(new ExpressionValidator(slots.keySet(), functions), root); + this.compiledExpression = new ExpressionCompiler().compileExpression(root, functions); + } + + public static Expression compile(String expression, String... variableNames) throws ExpressionException { + return new Expression(expression, variableNames); + } + + public static Expression getInstance() { + return instance.get().peek(); } public double evaluate(double... values) throws EvaluationException { @@ -177,7 +178,7 @@ public class Expression { private Double evaluateRoot() throws EvaluationException { pushInstance(); try { - return root.accept(new EvaluatingVisitor(slots, functions)); + return compiledExpression.execute(new ExecutionData(slots, functions)); } finally { popInstance(); } @@ -196,10 +197,6 @@ public class Expression { return slots; } - public static Expression getInstance() { - return instance.get().peek(); - } - private void pushInstance() { Stack threadLocalExprStack = instance.get(); if (threadLocalExprStack == null) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java index f1689f78e..fe44ee0ae 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/ExpressionHelper.java @@ -33,33 +33,51 @@ import java.util.stream.Collectors; import static com.sk89q.worldedit.antlr.ExpressionLexer.ID; -class ExpressionHelper { +public class ExpressionHelper { - static void check(boolean condition, ParserRuleContext ctx, String message) { + /** + * The argument should be wrapped in a {@link LocalSlot.Constant} before being passed. + */ + public static final String WRAPPED_CONSTANT = ""; + + private ExpressionHelper() { + } + + public static void check(boolean condition, ParserRuleContext ctx, String message) { if (!condition) { throw evalException(ctx, message); } } - static EvaluationException evalException(ParserRuleContext ctx, String message) { + public static int getErrorPosition(Token token) { + return token.getCharPositionInLine(); + } + + public static EvaluationException evalException(ParserRuleContext ctx, String message) { + return evalException(ctx.start, message); + } + + public static EvaluationException evalException(Token token, String message) { return new EvaluationException( - ctx.getStart().getCharPositionInLine(), - message + getErrorPosition(token), + message ); } - static void checkIterations(int iterations, ParserRuleContext ctx) { + public static void checkIterations(int iterations, ParserRuleContext ctx) { check(iterations <= 256, ctx, "Loop exceeded 256 iterations"); } - static void checkTimeout() { + // Special argument handle names + + public static void checkTimeout() { if (Thread.interrupted()) { throw new ExpressionTimeoutException("Calculations exceeded time limit."); } } - static MethodHandle resolveFunction(SetMultimap functions, - ExpressionParser.FunctionCallContext ctx) { + public static MethodHandle resolveFunction(SetMultimap functions, + ExpressionParser.FunctionCallContext ctx) { String fnName = ctx.name.getText(); Set matchingFns = functions.get(fnName); check(!matchingFns.isEmpty(), ctx, "Unknown function '" + fnName + "'"); @@ -79,34 +97,28 @@ class ExpressionHelper { } // We matched no function, fail with appropriate message. String possibleCounts = matchingFns.stream() - .map(mh -> mh.isVarargsCollector() - ? (mh.type().parameterCount() - 1) + "+" - : String.valueOf(mh.type().parameterCount())) - .collect(Collectors.joining("/")); + .map(mh -> mh.isVarargsCollector() + ? (mh.type().parameterCount() - 1) + "+" + : String.valueOf(mh.type().parameterCount())) + .collect(Collectors.joining("/")); throw evalException(ctx, "Incorrect number of arguments for function '" + fnName + "', " + - "expected " + possibleCounts + ", " + - "got " + ctx.args.size()); + "expected " + possibleCounts + ", " + + "got " + ctx.args.size()); } - // Special argument handle names - /** - * The argument should be wrapped in a {@link LocalSlot.Constant} before being passed. - */ - static final String WRAPPED_CONSTANT = ""; - /** * If this argument needs a handle, returns the name of the handle needed. Otherwise, returns * {@code null}. If {@code arg} isn't a valid handle reference, throws. */ - static String getArgumentHandleName(String fnName, MethodType type, int i, - ParserRuleContext arg) { + public static String getArgumentHandleName(String fnName, MethodType type, int i, + ParserRuleContext arg) { // Pass variable handle in for modification? Class pType = type.parameterType(i); Optional id = tryResolveId(arg); if (pType == LocalSlot.Variable.class) { // MUST be an id check(id.isPresent(), arg, - "Function '" + fnName + "' requires a variable in parameter " + i); + "Function '" + fnName + "' requires a variable in parameter " + i); return id.get(); } else if (pType == LocalSlot.class) { return id.orElse(WRAPPED_CONSTANT); @@ -116,7 +128,7 @@ class ExpressionHelper { private static Optional tryResolveId(ParserRuleContext arg) { Optional wrappedExprContext = - tryAs(arg, ExpressionParser.WrappedExprContext.class); + tryAs(arg, ExpressionParser.WrappedExprContext.class); if (wrappedExprContext.isPresent()) { return tryResolveId(wrappedExprContext.get().expression()); } @@ -127,8 +139,8 @@ class ExpressionHelper { } private static Optional tryAs( - ParserRuleContext ctx, - Class rule + ParserRuleContext ctx, + Class rule ) { if (rule.isInstance(ctx)) { return Optional.of(rule.cast(ctx)); @@ -143,7 +155,4 @@ class ExpressionHelper { return tryAs(ctxs.get(0), rule); } - private ExpressionHelper() { - } - -} +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java index e97f96ade..b033c72d3 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/Functions.java @@ -23,6 +23,7 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.SetMultimap; +import com.google.common.collect.Multimaps; import com.google.common.primitives.Doubles; import com.sk89q.worldedit.internal.expression.LocalSlot.Variable; import com.sk89q.worldedit.math.Vector3; @@ -36,6 +37,8 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.util.concurrent.ThreadLocalRandom; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.invoke.MethodHandles.filterReturnValue; import static java.lang.invoke.MethodType.methodType; /** @@ -56,7 +59,35 @@ final class Functions { throw new IllegalStateException(e); } - return ImmutableSetMultimap.copyOf(map); + // clean up all the functions + return ImmutableSetMultimap.copyOf( + Multimaps.transformValues(map, Functions::clean) + ); + } + + private static final MethodHandle DOUBLE_VALUE; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + DOUBLE_VALUE = lookup.findVirtual(Number.class, "doubleValue", + methodType(double.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + private static MethodHandle clean(MethodHandle handle) { + // box it all first + handle = handle.asType(handle.type().wrap()); + if (handle.type().returnType() != Double.class) { + // Ensure that the handle returns a Double, even if originally a Number + checkState(Number.class.isAssignableFrom(handle.type().returnType()), + "Function does not return a number"); + handle = handle.asType(handle.type().changeReturnType(Number.class)); + handle = filterReturnValue(handle, DOUBLE_VALUE); + } + return handle; } private static void addMathHandles( diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java new file mode 100644 index 000000000..3981b6749 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/CompilingVisitor.java @@ -0,0 +1,653 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression.invoke; + +import com.google.common.collect.SetMultimap; +import com.sk89q.worldedit.antlr.ExpressionBaseVisitor; +import com.sk89q.worldedit.antlr.ExpressionParser; +import com.sk89q.worldedit.internal.expression.*; +import it.unimi.dsi.fastutil.doubles.Double2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; +import org.antlr.v4.runtime.CommonToken; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.RuleNode; +import org.antlr.v4.runtime.tree.TerminalNode; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.List; +import java.util.function.DoubleBinaryOperator; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static com.sk89q.worldedit.antlr.ExpressionLexer.*; +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.WRAPPED_CONSTANT; +import static com.sk89q.worldedit.internal.expression.invoke.ExpressionHandles.*; +import static java.lang.invoke.MethodType.methodType; + +/** + * The brains of {@link ExpressionCompiler}. + */ +class CompilingVisitor extends ExpressionBaseVisitor { + + private static final MethodHandle BREAK_STATEMENT = + ExpressionHandles.dropData(MethodHandles.throwException(Double.class, BreakException.class) + .bindTo(BreakException.BREAK)); + private static final MethodHandle CONTINUE_STATEMENT = + ExpressionHandles.dropData(MethodHandles.throwException(Double.class, BreakException.class) + .bindTo(BreakException.CONTINUE)); + private static final double[] factorials = new double[171]; + /** + * Method handle (ExecutionData)Double, returns null. + */ + private static final MethodHandle DEFAULT_RESULT = + ExpressionHandles.dropData(MethodHandles.constant(Double.class, null)); + + static { + factorials[0] = 1; + for (int i = 1; i < factorials.length; ++i) { + factorials[i] = factorials[i - 1] * i; + } + } + + /* + * General idea is that we don't need to pass around variables, they're all in ExecutionData. + * We do need to pass that around, so most MethodHandles will be of the type + * (ExecutionData)Double, with a few as (ExecutionData,Double)Double where it needs an existing + * value passed in. EVERY handle returned from an overriden method must be of the first type. + */ + private final SetMultimap functions; + + CompilingVisitor(SetMultimap functions) { + this.functions = functions; + } + + // Usable AlmostEqual function, based on http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm + private static boolean almostEqual2sComplement(double a, double b, long maxUlps) { + // Make sure maxUlps is non-negative and small enough that the + // default NAN won't compare as equal to anything. + //assert(maxUlps > 0 && maxUlps < 4 * 1024 * 1024); // this is for floats, not doubles + + long aLong = Double.doubleToRawLongBits(a); + // Make aLong lexicographically ordered as a twos-complement long + if (aLong < 0) aLong = 0x8000000000000000L - aLong; + + long bLong = Double.doubleToRawLongBits(b); + // Make bLong lexicographically ordered as a twos-complement long + if (bLong < 0) bLong = 0x8000000000000000L - bLong; + + final long longDiff = Math.abs(aLong - bLong); + return longDiff <= maxUlps; + } + + private static double factorial(double x) throws EvaluationException { + final int n = (int) x; + + if (n < 0) { + return 0; + } + + if (n >= factorials.length) { + return Double.POSITIVE_INFINITY; + } + + return factorials[n]; + } + + private Token extractToken(ParserRuleContext ctx) { + List children = ctx.children.stream() + .filter(TerminalNode.class::isInstance) + .map(TerminalNode.class::cast) + .collect(Collectors.toList()); + ExpressionHelper.check(children.size() == 1, ctx, "Expected exactly one token, got " + children.size()); + return children.get(0).getSymbol(); + } + + private ExecNode evaluate(ParserRuleContext ctx) { + MethodHandle mh = ctx.accept(this); + if (ctx.parent instanceof ParserRuleContext) { + checkHandle(mh, (ParserRuleContext) ctx.parent); + } + return new ExecNode(ctx, mh); + } + + private void checkHandle(MethodHandle mh, ParserRuleContext ctx) { + ExpressionHelper.check(mh.type().equals(ExpressionHandles.COMPILED_EXPRESSION_SIG), ctx, + "Incorrect type returned from handler for " + ctx.getClass()); + } + + private MethodHandle evaluateForNamedValue(ParserRuleContext ctx, String name) { + MethodHandle guard = MethodHandles.guardWithTest( + // if result is null + IS_NULL.asType(methodType(boolean.class, Double.class)), + // throw appropriate exception, dropping `result` argument + MethodHandles.dropArguments( + ExpressionHandles.throwEvalException(ctx, "Invalid expression for " + name), 0, Double.class + ), + // else return the argument we were passed + MethodHandles.identity(Double.class) + ); + // now pass `result` into `guard` + MethodHandle result = evaluate(ctx).handle; + return MethodHandles.collectArguments(guard, 0, result); + } + + private MethodHandle evaluateForValue(ParserRuleContext ctx) { + return evaluateForNamedValue(ctx, "a value"); + } + + private MethodHandle evaluateBoolean(ParserRuleContext boolExpression) { + MethodHandle value = evaluateForNamedValue(boolExpression, "a boolean"); + value = value.asType(value.type().unwrap()); + // Pass `value` into converter, returns (ExecutionData)boolean; + return MethodHandles.collectArguments( + DOUBLE_TO_BOOL, 0, value + ); + } + + private MethodHandle evaluateConditional(ParserRuleContext condition, + ParserRuleContext trueBranch, + ParserRuleContext falseBranch) { + // easiest one of the bunch + return MethodHandles.guardWithTest( + evaluateBoolean(condition), + trueBranch == null ? NULL_DOUBLE : evaluate(trueBranch).handle, + falseBranch == null ? NULL_DOUBLE : evaluate(falseBranch).handle + ); + } + + @Override + public MethodHandle visitIfStatement(ExpressionParser.IfStatementContext ctx) { + return evaluateConditional(ctx.condition, ctx.trueBranch, ctx.falseBranch); + } + + @Override + public MethodHandle visitTernaryExpr(ExpressionParser.TernaryExprContext ctx) { + return evaluateConditional(ctx.condition, ctx.trueBranch, ctx.falseBranch); + } + + @Override + public MethodHandle visitWhileStatement(ExpressionParser.WhileStatementContext ctx) { + return ExpressionHandles.whileLoop( + evaluateBoolean(ctx.condition), + evaluate(ctx.body) + ); + } + + @Override + public MethodHandle visitDoStatement(ExpressionParser.DoStatementContext ctx) { + return ExpressionHandles.doWhileLoop( + evaluateBoolean(ctx.condition), + evaluate(ctx.body) + ); + } + + @Override + public MethodHandle visitForStatement(ExpressionParser.ForStatementContext ctx) { + return ExpressionHandles.forLoop( + evaluate(ctx.init).handle, + evaluateBoolean(ctx.condition), + evaluate(ctx.body), + evaluate(ctx.update).handle + ); + } + + @Override + public MethodHandle visitSimpleForStatement(ExpressionParser.SimpleForStatementContext ctx) { + return ExpressionHandles.simpleForLoop( + evaluateForValue(ctx.first), + evaluateForValue(ctx.last), + ctx.counter, + evaluate(ctx.body) + ); + } + + @Override + public MethodHandle visitBreakStatement(ExpressionParser.BreakStatementContext ctx) { + return BREAK_STATEMENT; + } + + @Override + public MethodHandle visitContinueStatement(ExpressionParser.ContinueStatementContext ctx) { + return CONTINUE_STATEMENT; + } + + @Override + public MethodHandle visitReturnStatement(ExpressionParser.ReturnStatementContext ctx) { + if (ctx.value != null) { + return evaluate(ctx.value).handle; + } + return defaultResult(); + } + + @Override + public MethodHandle visitSwitchStatement(ExpressionParser.SwitchStatementContext ctx) { + Double2ObjectMap cases = new Double2ObjectLinkedOpenHashMap<>(ctx.labels.size()); + ExecNode defaultCase = null; + for (int i = 0; i < ctx.labels.size(); i++) { + ExpressionParser.SwitchLabelContext label = ctx.labels.get(i); + ExpressionParser.StatementsContext body = ctx.bodies.get(i); + ExecNode node = evaluate(body); + if (label instanceof ExpressionParser.CaseContext) { + ExpressionParser.CaseContext caseContext = (ExpressionParser.CaseContext) label; + double key = (double) ExpressionHandles.constantInvoke(evaluateForValue(caseContext.constant)); + ExpressionHelper.check(!cases.containsKey(key), body, "Duplicate cases detected."); + cases.put(key, node); + } else { + ExpressionHelper.check(defaultCase == null, body, "Duplicate default cases detected."); + defaultCase = node; + } + } + return ExpressionHandles.switchStatement(cases, evaluateForValue(ctx.target), defaultCase); + } + + @Override + public MethodHandle visitExpressionStatement(ExpressionParser.ExpressionStatementContext ctx) { + return evaluate(ctx.expression()).handle; + } + + @Override + public MethodHandle visitPostCrementExpr(ExpressionParser.PostCrementExprContext ctx) { + Token target = ctx.target; + int opType = ctx.op.getType(); + return ExpressionHandles.call(data -> { + LocalSlot.Variable variable = ExpressionHandles.getVariable(data, target); + double value = variable.getValue(); + if (opType == INCREMENT) { + value++; + } else { + value--; + } + variable.setValue(value); + return value; + }); + } + + @Override + public MethodHandle visitPreCrementExpr(ExpressionParser.PreCrementExprContext ctx) { + Token target = ctx.target; + int opType = ctx.op.getType(); + return ExpressionHandles.call(data -> { + LocalSlot.Variable variable = ExpressionHandles.getVariable(data, target); + double value = variable.getValue(); + double result = value; + if (opType == INCREMENT) { + value++; + } else { + value--; + } + variable.setValue(value); + return result; + }); + } + + @Override + public MethodHandle visitPlusMinusExpr(ExpressionParser.PlusMinusExprContext ctx) { + MethodHandle value = evaluateForValue(ctx.expr); + switch (ctx.op.getType()) { + case PLUS: + return value; + case MINUS: + return ExpressionHandles.call(data -> + -(double) ExpressionHandles.standardInvoke(value, data) + ); + } + throw ExpressionHelper.evalException(ctx, "Invalid text for plus/minus expr: " + ctx.op.getText()); + } + + @Override + public MethodHandle visitNotExpr(ExpressionParser.NotExprContext ctx) { + MethodHandle expr = evaluateBoolean(ctx.expr); + return ExpressionHandles.call(data -> + ExpressionHandles.boolToDouble(!(boolean) ExpressionHandles.standardInvoke(expr, data)) + ); + } + + @Override + public MethodHandle visitComplementExpr(ExpressionParser.ComplementExprContext ctx) { + MethodHandle expr = evaluateForValue(ctx.expr); + // Looks weird. In order: + // - Convert back to double from following long + // - Convert to long from double value + // - Convert from Object to Double to double. + return ExpressionHandles.call(data -> + (double) ~(long) (double) ExpressionHandles.standardInvoke(expr, data) + ); + } + + @Override + public MethodHandle visitConditionalAndExpr(ExpressionParser.ConditionalAndExprContext ctx) { + MethodHandle left = evaluateBoolean(ctx.left); + MethodHandle right = evaluateForValue(ctx.right); + return MethodHandles.guardWithTest( + left, + right, + ExpressionHandles.dropData( + MethodHandles.constant(Double.class, ExpressionHandles.boolToDouble(false)) + ) + ); + } + + @Override + public MethodHandle visitConditionalOrExpr(ExpressionParser.ConditionalOrExprContext ctx) { + MethodHandle left = evaluateForValue(ctx.left); + MethodHandle right = evaluateForValue(ctx.right); + // Inject left as primary condition, on failure take right with data parameter + // logic = (Double,ExecutionData)Double + MethodHandle logic = MethodHandles.guardWithTest( + // data arg dropped implicitly + DOUBLE_TO_BOOL, + // drop data arg + MethodHandles.dropArguments( + MethodHandles.identity(Double.class), 1, ExecutionData.class + ), + // drop left arg, call right + MethodHandles.dropArguments( + right, 0, Double.class + ) + ); + // mixed = (ExecutionData,ExecutionData)Double + MethodHandle mixed = MethodHandles.collectArguments( + logic, 0, left + ); + // Deduplicate ExecutionData + return ExpressionHandles.dedupData(mixed); + } + + private MethodHandle evaluateBinary(ParserRuleContext left, + ParserRuleContext right, + DoubleBinaryOperator op) { + MethodHandle mhLeft = evaluateForValue(left); + MethodHandle mhRight = evaluateForValue(right); + // Map two data args to two double args, then evaluate op + MethodHandle doubleData = MethodHandles.filterArguments( + CALL_BINARY_OP.bindTo(op), 0, + mhLeft.asType(mhLeft.type().unwrap()), mhRight.asType(mhRight.type().unwrap()) + ); + doubleData = doubleData.asType(doubleData.type().wrap()); + return ExpressionHandles.dedupData(doubleData); + } + + private MethodHandle evaluateBinary(ParserRuleContext left, + ParserRuleContext right, + Supplier op) { + return evaluateBinary(left, right, op.get()); + } + + @Override + public MethodHandle visitPowerExpr(ExpressionParser.PowerExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, Math::pow); + } + + @Override + public MethodHandle visitMultiplicativeExpr(ExpressionParser.MultiplicativeExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, () -> { + switch (ctx.op.getType()) { + case TIMES: + return (l, r) -> l * r; + case DIVIDE: + return (l, r) -> l / r; + case MODULO: + return (l, r) -> l % r; + } + throw ExpressionHelper.evalException(ctx, "Invalid text for multiplicative expr: " + ctx.op.getText()); + }); + } + + @Override + public MethodHandle visitAddExpr(ExpressionParser.AddExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, () -> { + switch (ctx.op.getType()) { + case PLUS: + return Double::sum; + case MINUS: + return (l, r) -> l - r; + } + throw ExpressionHelper.evalException(ctx, "Invalid text for additive expr: " + ctx.op.getText()); + }); + } + + @Override + public MethodHandle visitShiftExpr(ExpressionParser.ShiftExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, () -> { + switch (ctx.op.getType()) { + case LEFT_SHIFT: + return (l, r) -> (double) ((long) l << (long) r); + case RIGHT_SHIFT: + return (l, r) -> (double) ((long) l >> (long) r); + } + throw ExpressionHelper.evalException(ctx, "Invalid text for shift expr: " + ctx.op.getText()); + }); + } + + @Override + public MethodHandle visitRelationalExpr(ExpressionParser.RelationalExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, () -> { + switch (ctx.op.getType()) { + case LESS_THAN: + return (l, r) -> ExpressionHandles.boolToDouble(l < r); + case LESS_THAN_OR_EQUAL: + return (l, r) -> ExpressionHandles.boolToDouble(l <= r); + case GREATER_THAN: + return (l, r) -> ExpressionHandles.boolToDouble(l > r); + case GREATER_THAN_OR_EQUAL: + return (l, r) -> ExpressionHandles.boolToDouble(l >= r); + } + throw ExpressionHelper.evalException(ctx, "Invalid text for relational expr: " + ctx.op.getText()); + }); + } + + @Override + public MethodHandle visitEqualityExpr(ExpressionParser.EqualityExprContext ctx) { + return evaluateBinary(ctx.left, ctx.right, () -> { + switch (ctx.op.getType()) { + case EQUAL: + return (l, r) -> ExpressionHandles.boolToDouble(l == r); + case NOT_EQUAL: + return (l, r) -> ExpressionHandles.boolToDouble(l != r); + case NEAR: + return (l, r) -> ExpressionHandles.boolToDouble(almostEqual2sComplement(l, r, 450359963L)); + case GREATER_THAN_OR_EQUAL: + return (l, r) -> ExpressionHandles.boolToDouble(l >= r); + } + throw ExpressionHelper.evalException(ctx, "Invalid text for equality expr: " + ctx.op.getText()); + }); + } + + @Override + public MethodHandle visitPostfixExpr(ExpressionParser.PostfixExprContext ctx) { + MethodHandle value = evaluateForValue(ctx.expr); + if (ctx.op.getType() == EXCLAMATION_MARK) { + return ExpressionHandles.call(data -> + factorial((double) ExpressionHandles.standardInvoke(value, data)) + ); + } + throw ExpressionHelper.evalException(ctx, + "Invalid text for post-unary expr: " + ctx.op.getText()); + } + + @Override + public MethodHandle visitAssignment(ExpressionParser.AssignmentContext ctx) { + int type = extractToken(ctx.assignmentOperator()).getType(); + Token target = ctx.target; + MethodHandle getArg = evaluateForValue(ctx.expression()); + return ExpressionHandles.call(data -> { + double value; + double arg = (double) ExpressionHandles.standardInvoke(getArg, data); + LocalSlot.Variable variable; + if (type == ASSIGN) { + variable = ExpressionHandles.initVariable(data, target); + value = arg; + } else { + variable = ExpressionHandles.getVariable(data, target); + value = variable.getValue(); + switch (type) { + case POWER_ASSIGN: + value = Math.pow(value, arg); + break; + case TIMES_ASSIGN: + value *= arg; + break; + case DIVIDE_ASSIGN: + value /= arg; + break; + case MODULO_ASSIGN: + value %= arg; + break; + case PLUS_ASSIGN: + value += arg; + break; + case MINUS_ASSIGN: + value -= arg; + break; + default: + throw ExpressionHelper.evalException(ctx, "Invalid text for assign expr: " + + ctx.assignmentOperator().getText()); + } + } + variable.setValue(value); + return value; + }); + } + + @Override + public MethodHandle visitFunctionCall(ExpressionParser.FunctionCallContext ctx) { + MethodHandle handle = ExpressionHelper.resolveFunction(functions, ctx); + String fnName = ctx.name.getText(); + MethodHandle[] arguments = new MethodHandle[ctx.args.size()]; + for (int i = 0; i < arguments.length; i++) { + ExpressionParser.ExpressionContext arg = ctx.args.get(i); + MethodHandle transformed = getArgument(fnName, handle.type(), i, arg); + Class ptype = handle.type().parameterType(i); + Class rtype = transformed.type().returnType(); + if (ptype != rtype && ptype.isAssignableFrom(rtype)) { + // need to upcast + transformed = transformed.asType(transformed.type().changeReturnType(ptype)); + } + arguments[i] = transformed; + } + // Take each of our data accepting arguments, apply them over the source method + MethodHandle manyData = MethodHandles.filterArguments(handle, 0, arguments); + // Collapse every data into one argument + int[] permutation = new int[arguments.length]; + return MethodHandles.permuteArguments( + manyData, ExpressionHandles.COMPILED_EXPRESSION_SIG, permutation + ); + } + + // MH: (ExecutionData)T; (depends on target) + private MethodHandle getArgument(String fnName, MethodType type, int i, ParserRuleContext arg) { + // Pass variable handle in for modification? + String handleName = ExpressionHelper.getArgumentHandleName(fnName, type, i, arg); + if (handleName == null) { + return evaluateForValue(arg); + } + if (handleName.equals(WRAPPED_CONSTANT)) { + // pass arg into new LocalSlot.Constant + MethodHandle filter = evaluateForValue(arg); + filter = filter.asType(filter.type().unwrap()); + return MethodHandles.collectArguments( + NEW_LS_CONSTANT, 0, filter + ); + } + // small hack + CommonToken fake = new CommonToken(arg.start); + fake.setText(handleName); + return ExpressionHandles.mhGetVariable(fake); + } + + @Override + public MethodHandle visitConstantExpression(ExpressionParser.ConstantExpressionContext ctx) { + try { + return ExpressionHandles.dropData( + MethodHandles.constant(Double.class, Double.parseDouble(ctx.getText())) + ); + } catch (NumberFormatException e) { + // Rare, but might happen, e.g. if too many digits + throw ExpressionHelper.evalException(ctx, "Invalid constant: " + e.getMessage()); + } + } + + @Override + public MethodHandle visitIdExpr(ExpressionParser.IdExprContext ctx) { + Token source = ctx.source; + return ExpressionHandles.call(data -> ExpressionHandles.getSlotValue(data, source)); + } + + @Override + protected MethodHandle defaultResult() { + return DEFAULT_RESULT; + } + + @Override + public MethodHandle visitChildren(RuleNode node) { + MethodHandle result = defaultResult(); + int n = node.getChildCount(); + for (int i = 0; i < n; i++) { + ParseTree c = node.getChild(i); + if (c instanceof TerminalNode && ((TerminalNode) c).getSymbol().getType() == Token.EOF) { + break; + } + + MethodHandle childResult = c.accept(this); + if (c instanceof ParserRuleContext) { + checkHandle(childResult, (ParserRuleContext) c); + } + + boolean returning = c instanceof ExpressionParser.ReturnStatementContext; + result = aggregateResult(result, childResult, returning); + if (returning) { + return result; + } + } + + return result; + } + + @Override + protected MethodHandle aggregateResult(MethodHandle aggregate, MethodHandle nextResult) { + throw new UnsupportedOperationException(); + } + + private MethodHandle aggregateResult(MethodHandle oldResult, MethodHandle result, + boolean keepDefault) { + // Execute `oldResult` but ignore its return value, then execute result and return that. + // If `oldResult` (the old value) is `defaultResult`, it's bogus, so just skip it + if (oldResult == DEFAULT_RESULT) { + return result; + } + if (result == DEFAULT_RESULT && !keepDefault) { + return oldResult; + } + // Add a dummy Double parameter to the end + MethodHandle dummyDouble = MethodHandles.dropArguments( + result, 1, Double.class + ); + // Have oldResult turn it from data->Double + MethodHandle doubledData = MethodHandles.collectArguments( + dummyDouble, 1, oldResult + ); + // Deduplicate the `data` parameter + return ExpressionHandles.dedupData(doubledData); + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExecNode.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExecNode.java new file mode 100644 index 000000000..06791b8a9 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExecNode.java @@ -0,0 +1,34 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression.invoke; + +import org.antlr.v4.runtime.ParserRuleContext; + +import java.lang.invoke.MethodHandle; + +class ExecNode { + final ParserRuleContext ctx; + final MethodHandle handle; + + ExecNode(ParserRuleContext ctx, MethodHandle handle) { + this.ctx = ctx; + this.handle = handle; + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java new file mode 100644 index 000000000..5eafc9d01 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionCompiler.java @@ -0,0 +1,73 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression.invoke; + +import com.google.common.collect.SetMultimap; +import com.sk89q.worldedit.antlr.ExpressionParser; +import com.sk89q.worldedit.internal.expression.CompiledExpression; + +import java.lang.invoke.LambdaConversionException; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import static java.lang.invoke.MethodType.methodType; + +/** + * Compiles an expression from an AST into {@link MethodHandle}s. + */ +public class ExpressionCompiler { + + private static final String CE_EXECUTE = "execute"; + private static final MethodType HANDLE_TO_CE = + methodType(CompiledExpression.class, MethodHandle.class); + + private static final MethodHandle HANDLE_TO_CE_CONVERTER; + + static { + MethodHandle handleInvoker = MethodHandles.invoker(ExpressionHandles.COMPILED_EXPRESSION_SIG); + try { + HANDLE_TO_CE_CONVERTER = LambdaMetafactory.metafactory( + MethodHandles.lookup(), + // Implementing CompiledExpression.execute + CE_EXECUTE, + // Take a handle, to be converted to CompiledExpression + HANDLE_TO_CE, + // Raw signature for SAM type + ExpressionHandles.COMPILED_EXPRESSION_SIG, + // Handle to call the captured handle. + handleInvoker, + // Actual signature at invoke time + ExpressionHandles.COMPILED_EXPRESSION_SIG + ).dynamicInvoker().asType(HANDLE_TO_CE); + } catch (LambdaConversionException e) { + throw new IllegalStateException("Failed to load ExpressionCompiler MetaFactory", e); + } + } + + public CompiledExpression compileExpression(ExpressionParser.AllStatementsContext root, + SetMultimap functions) { + MethodHandle invokable = root.accept(new CompilingVisitor(functions)); + return (CompiledExpression) ExpressionHandles.safeInvoke( + HANDLE_TO_CE_CONVERTER, h -> h.invoke(invokable) + ); + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java new file mode 100644 index 000000000..ac6e8bca8 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/expression/invoke/ExpressionHandles.java @@ -0,0 +1,339 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.expression.invoke; + +import com.google.common.base.Throwables; +import com.sk89q.worldedit.internal.expression.*; +import it.unimi.dsi.fastutil.doubles.Double2ObjectMap; +import it.unimi.dsi.fastutil.doubles.Double2ObjectMaps; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; + +import javax.annotation.Nullable; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Objects; +import java.util.function.DoubleBinaryOperator; +import java.util.function.Supplier; + +import static com.sk89q.worldedit.internal.expression.ExpressionHelper.*; +import static java.lang.invoke.MethodHandles.*; +import static java.lang.invoke.MethodType.methodType; + +class ExpressionHandles { + + static final MethodType COMPILED_EXPRESSION_SIG = methodType(Double.class, ExecutionData.class); + static final MethodHandle IS_NULL; + static final MethodHandle DOUBLE_TO_BOOL; + static final MethodHandle CALL_BINARY_OP; + static final MethodHandle NEW_LS_CONSTANT; + static final MethodHandle NULL_DOUBLE = dropData(constant(Double.class, null)); + private static final MethodHandle EVAL_EXCEPTION_CONSTR; + private static final MethodHandle CALL_EXPRESSION; + private static final MethodHandle GET_VARIABLE; + private static final MethodHandle WHILE_FOR_LOOP_IMPL; + private static final MethodHandle DO_WHILE_LOOP_IMPL; + private static final MethodHandle SIMPLE_FOR_LOOP_IMPL; + private static final MethodHandle SWITCH_IMPL; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + EVAL_EXCEPTION_CONSTR = lookup.findConstructor( + EvaluationException.class, methodType(void.class, int.class, String.class)); + CALL_EXPRESSION = lookup.findVirtual( + CompiledExpression.class, "execute", + methodType(Double.class, ExecutionData.class)); + GET_VARIABLE = lookup.findStatic(ExpressionHandles.class, "getVariable", + methodType(LocalSlot.Variable.class, ExecutionData.class, Token.class)); + WHILE_FOR_LOOP_IMPL = lookup.findStatic(ExpressionHandles.class, + "whileForLoopImpl", + methodType(Double.class, ExecutionData.class, MethodHandle.class, + MethodHandle.class, ExecNode.class, MethodHandle.class)); + DO_WHILE_LOOP_IMPL = lookup.findStatic(ExpressionHandles.class, "doWhileLoopImpl", + methodType(Double.class, ExecutionData.class, MethodHandle.class, ExecNode.class)); + SIMPLE_FOR_LOOP_IMPL = lookup.findStatic(ExpressionHandles.class, "simpleForLoopImpl", + methodType(Double.class, ExecutionData.class, MethodHandle.class, + MethodHandle.class, Token.class, ExecNode.class)); + SWITCH_IMPL = lookup.findStatic(ExpressionHandles.class, "switchImpl", + methodType(Double.class, ExecutionData.class, Double2ObjectMap.class, + MethodHandle.class, ExecNode.class)); + + IS_NULL = lookup.findStatic(Objects.class, "isNull", + methodType(boolean.class, Object.class)); + DOUBLE_TO_BOOL = lookup.findStatic(ExpressionHandles.class, "doubleToBool", + methodType(boolean.class, double.class)); + CALL_BINARY_OP = lookup.findVirtual(DoubleBinaryOperator.class, "applyAsDouble", + methodType(double.class, double.class, double.class)); + NEW_LS_CONSTANT = lookup.findConstructor(LocalSlot.Constant.class, + methodType(void.class, double.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + private ExpressionHandles() { + } + + static Object safeInvoke(MethodHandle handle, Invokable invokable) { + try { + return invokable.invoke(handle); + } catch (Throwable t) { + Throwables.throwIfUnchecked(t); + throw new RuntimeException(t); + } + } + + static Object standardInvoke(MethodHandle handle, ExecutionData data) { + return safeInvoke(handle, h -> h.invoke(data)); + } + + static Object constantInvoke(MethodHandle handle) { + return standardInvoke(handle, ExecutionData.CONSTANT_EVALUATOR); + } + + static MethodHandle dropData(MethodHandle handle) { + return dropArguments(handle, 0, ExecutionData.class); + } + + static MethodHandle dedupData(MethodHandle doubleData) { + return permuteArguments( + doubleData, COMPILED_EXPRESSION_SIG, + 0, 0 + ); + } + + static LocalSlot.Variable initVariable(ExecutionData data, Token nameToken) { + String name = nameToken.getText(); + return data.getSlots().initVariable(name) + .orElseThrow(() -> ExpressionHelper.evalException( + nameToken, "Cannot overwrite non-variable '" + name + "'" + )); + } + + private static Supplier varNotInitException(Token nameToken) { + return () -> ExpressionHelper.evalException( + nameToken, "'" + nameToken.getText() + "' is not initialized yet" + ); + } + + static MethodHandle mhGetVariable(Token nameToken) { + return insertArguments(GET_VARIABLE, 1, nameToken); + } + + static LocalSlot.Variable getVariable(ExecutionData data, Token nameToken) { + String name = nameToken.getText(); + LocalSlot slot = data.getSlots().getSlot(name) + .orElseThrow(varNotInitException(nameToken)); + if (!(slot instanceof LocalSlot.Variable)) { + throw ExpressionHelper.evalException( + nameToken, "'" + name + "' is not a variable" + ); + } + return (LocalSlot.Variable) slot; + } + + static double getSlotValue(ExecutionData data, Token nameToken) { + String name = nameToken.getText(); + return data.getSlots().getSlotValue(name) + .orElseThrow(varNotInitException(nameToken)); + } + + /** + * Returns a method handle that calls + * {@link EvaluationException#EvaluationException(int, String)} with the supplied arguments. + */ + private static MethodHandle evalException(ParserRuleContext ctx, String message) { + return insertArguments(EVAL_EXCEPTION_CONSTR, 0, + getErrorPosition(ctx.start), message); + } + + /** + * Returns a method handle that takes no arguments, and throws the result of + * {@link #evalException(ParserRuleContext, String)}. It will additionally return Double. + */ + static MethodHandle throwEvalException(ParserRuleContext ctx, String message) { + // replace arg0 of `throw` with `evalException` + return collectArguments( + throwException(Double.class, EvaluationException.class), + 0, + evalException(ctx, message) + ); + } + + private static boolean doubleToBool(double bool) { + return bool > 0; + } + + static double boolToDouble(boolean bool) { + return bool ? 1 : 0; + } + + /** + * Encapsulate the given code into a MethodHandle. + */ + static MethodHandle call(CompiledExpression runnable) { + return CALL_EXPRESSION.bindTo(runnable).asType(COMPILED_EXPRESSION_SIG); + } + + static MethodHandle whileLoop(MethodHandle condition, ExecNode body) { + return insertArguments(WHILE_FOR_LOOP_IMPL, 1, + null, condition, body, null); + } + + static MethodHandle forLoop(MethodHandle init, + MethodHandle condition, + ExecNode body, + MethodHandle update) { + return insertArguments(WHILE_FOR_LOOP_IMPL, 1, + init, condition, body, update); + } + + private static Double whileForLoopImpl(ExecutionData data, + @Nullable MethodHandle init, + MethodHandle condition, + ExecNode body, + @Nullable MethodHandle update) { + Double result = null; + int iterations = 0; + if (init != null) { + standardInvoke(init, data); + } + while ((boolean) standardInvoke(condition, data)) { + checkIterations(iterations, body.ctx); + checkTimeout(); + iterations++; + try { + result = (Double) standardInvoke(body.handle, data); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + if (update != null) { + standardInvoke(update, data); + } + } + return result; + } + + static MethodHandle doWhileLoop(MethodHandle condition, ExecNode body) { + return insertArguments(DO_WHILE_LOOP_IMPL, 1, condition, body); + } + + private static Double doWhileLoopImpl(ExecutionData data, + MethodHandle condition, + ExecNode body) { + Double result = null; + int iterations = 0; + do { + checkIterations(iterations, body.ctx); + checkTimeout(); + iterations++; + try { + result = (Double) standardInvoke(body.handle, data); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + } while ((boolean) standardInvoke(condition, data)); + return result; + } + + static MethodHandle simpleForLoop(MethodHandle first, + MethodHandle last, + Token counter, + ExecNode body) { + return insertArguments(SIMPLE_FOR_LOOP_IMPL, 1, + first, last, counter, body); + } + + private static Double simpleForLoopImpl(ExecutionData data, + MethodHandle getFirst, + MethodHandle getLast, + Token counterToken, + ExecNode body) { + Double result = null; + int iterations = 0; + double first = (double) standardInvoke(getFirst, data); + double last = (double) standardInvoke(getLast, data); + LocalSlot.Variable variable = initVariable(data, counterToken); + for (double i = first; i <= last; i++) { + checkIterations(iterations, body.ctx); + checkTimeout(); + iterations++; + variable.setValue(i); + try { + result = (Double) standardInvoke(body.handle, data); + } catch (BreakException ex) { + if (!ex.doContinue) { + break; + } + } + } + return result; + } + + static MethodHandle switchStatement(Double2ObjectMap cases, + MethodHandle getValue, + @Nullable ExecNode defaultCase) { + return insertArguments(SWITCH_IMPL, 1, cases, getValue, defaultCase); + } + + private static Double switchImpl(ExecutionData data, + Double2ObjectMap cases, + MethodHandle getValue, + @Nullable ExecNode defaultCase) { + double value = (double) standardInvoke(getValue, data); + boolean matched = false; + Double evaluated = null; + boolean falling = false; + for (Double2ObjectMap.Entry entry : Double2ObjectMaps.fastIterable(cases)) { + if (falling || entry.getDoubleKey() == value) { + matched = true; + try { + evaluated = (Double) standardInvoke(entry.getValue().handle, data); + falling = true; + } catch (BreakException brk) { + check(!brk.doContinue, entry.getValue().ctx, "Cannot continue in a switch"); + falling = false; + break; + } + } + } + // This if is like the one in the loop, default's "case" is `!matched` & present + if ((falling || !matched) && defaultCase != null) { + try { + evaluated = (Double) standardInvoke(defaultCase.handle, data); + } catch (BreakException brk) { + check(!brk.doContinue, defaultCase.ctx, "Cannot continue in a switch"); + } + } + return evaluated; + } + + @FunctionalInterface + interface Invokable { + Object invoke(MethodHandle handle) throws Throwable; + } + +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java index f917a39f8..7ee536538 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/block/BlockType.java @@ -20,7 +20,6 @@ package com.sk89q.worldedit.world.block; import com.sk89q.worldedit.WorldEdit; -import com.google.common.collect.Iterables; import com.sk89q.worldedit.WorldEditException; import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.extent.Extent; @@ -33,20 +32,21 @@ import com.sk89q.worldedit.registry.NamespacedRegistry; import com.sk89q.worldedit.registry.state.AbstractProperty; import com.sk89q.worldedit.registry.state.Property; import com.sk89q.worldedit.registry.state.PropertyKey; -import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.util.concurrency.LazyReference; +import com.sk89q.worldedit.world.item.ItemType; import com.sk89q.worldedit.world.item.ItemTypes; import com.sk89q.worldedit.world.registry.BlockMaterial; import com.sk89q.worldedit.world.registry.LegacyMapper; + +import javax.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import static com.google.common.base.Preconditions.checkArgument; -import java.util.stream.IntStream; -import javax.annotation.Nullable; public class BlockType implements FawePattern, Keyed { @@ -54,7 +54,8 @@ public class BlockType implements FawePattern, Keyed { private final String id; private final BlockTypesCache.Settings settings; - + private final LazyReference legacyId = LazyReference.from(() -> computeLegacy(0)); + private final LazyReference legacyData = LazyReference.from(() -> computeLegacy(1)); private boolean initItemType; private ItemType itemType; @@ -103,6 +104,7 @@ public class BlockType implements FawePattern, Keyed { return name; } } + /* private BlockState computeDefaultState() { @@ -212,7 +214,7 @@ public class BlockType implements FawePattern, Keyed { */ AbstractProperty btp = this.settings.propertiesMap.get(prop.getName()); checkArgument(btp != null, "%s has no property named %s", this, prop.getName()); - id = btp.modify(id, btp.getValueFor((String)value)); + id = btp.modify(id, btp.getValueFor((String) value)); } return withStateId(id); } @@ -233,7 +235,7 @@ public class BlockType implements FawePattern, Keyed { */ @Nullable public ItemType getItemType() { - if(!initItemType) { + if (!initItemType) { initItemType = true; itemType = ItemTypes.get(this.id); } @@ -251,7 +253,7 @@ public class BlockType implements FawePattern, Keyed { /** * Gets the legacy ID. Needed for legacy reasons. - * + *

* DO NOT USE THIS. * * @return legacy id or 0, if unknown @@ -264,7 +266,7 @@ public class BlockType implements FawePattern, Keyed { /** * The internal index of this type. - * + *

* This number is not necessarily consistent across restarts. * * @return internal id @@ -288,7 +290,6 @@ public class BlockType implements FawePattern, Keyed { return obj == this; // stop changing this to a shitty string comparison } - @Override public boolean apply(Extent extent, BlockVector3 get, BlockVector3 set) throws WorldEditException { return set.setBlock(extent, getDefaultState()); @@ -307,14 +308,25 @@ public class BlockType implements FawePattern, Keyed { return new SingleBlockTypeMask(extent, this); } - @Deprecated public int getLegacyId() { - Integer id = LegacyMapper.getInstance().getLegacyCombined(this.getDefaultState()); - if (id != null) { - return id >> 4; - } else { - return 0; - } + return legacyId.getValue(); + } + + /** + * Gets the legacy data. Needed for legacy reasons. + *

+ * DO NOT USE THIS. + * + * @return legacy data or 0, if unknown + */ + @Deprecated + public int getLegacyData() { + return legacyData.getValue(); + } + + private int computeLegacy(int index) { + int[] legacy = LegacyMapper.getInstance().getLegacyFromBlock(this.getDefaultState()); + return legacy != null ? legacy[index] : 0; } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java index 030c667aa..167572da6 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/item/ItemType.java @@ -26,8 +26,11 @@ import com.sk89q.worldedit.extension.platform.Capability; import com.sk89q.worldedit.registry.Keyed; import com.sk89q.worldedit.registry.NamespacedRegistry; import com.sk89q.worldedit.registry.RegistryItem; +import com.sk89q.worldedit.util.concurrency.LazyReference; import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.registry.BlockMaterial; +import com.sk89q.worldedit.world.registry.ItemMaterial; import javax.annotation.Nullable; @@ -37,6 +40,9 @@ public class ItemType implements RegistryItem, Keyed { private String id; private String name; + private final LazyReference itemMaterial + = LazyReference.from(() -> WorldEdit.getInstance().getPlatformManager() + .queryCapability(Capability.GAME_HOOKS).getRegistries().getItemRegistry().getMaterial(this)); private BlockType blockType; private boolean initBlockType; private BaseItem defaultState; @@ -113,6 +119,15 @@ public class ItemType implements RegistryItem, Keyed { return this.defaultState; } + /** + * Get the material for this ItemType. + * + * @return The material + */ + public ItemMaterial getMaterial() { + return itemMaterial.getValue(); + } + @Override public String toString() { return getId(); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemData.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemData.java index 5afd55388..40ff378be 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemData.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemData.java @@ -114,6 +114,23 @@ public final class BundledItemData { return idMap.get(id); } + /** + * Get the material properties for the given item. + * + * @param id the string ID + * @return the material's properties, or null + */ + @Nullable + public ItemMaterial getMaterialById(String id) { + ItemEntry entry = findById(id); + if (entry != null) { + // FIXME: This should probably just be part of the JSON itself + return new SimpleItemMaterial(entry.maxStackSize, entry.maxDamage); + } else { + return null; + } + } + /** * Get a singleton instance of this object. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemRegistry.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemRegistry.java index 1ab788e42..c41e24cfc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemRegistry.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/BundledItemRegistry.java @@ -29,14 +29,18 @@ import javax.annotation.Nullable; */ public class BundledItemRegistry implements ItemRegistry { + private BundledItemData.ItemEntry getEntryById(ItemType itemType) { + return BundledItemData.getInstance().findById(itemType.getId()); + } + @Nullable @Override public String getName(ItemType itemType) { - String id = itemType.getId(); - BundledItemData.ItemEntry itemEntry = BundledItemData.getInstance().findById(id); + BundledItemData.ItemEntry itemEntry = getEntryById(itemType); if (itemEntry != null) { String localized = itemEntry.localizedName; if (localized.equals("Air")) { + String id = itemType.getId(); int c = id.indexOf(':'); return c < 0 ? id : id.substring(c + 1); } @@ -44,4 +48,10 @@ public class BundledItemRegistry implements ItemRegistry { } return null; } + + @Nullable + @Override + public ItemMaterial getMaterial(ItemType itemType) { + return new PassthroughItemMaterial(BundledItemData.getInstance().getMaterialById(itemType.getId())); + } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemMaterial.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemMaterial.java new file mode 100644 index 000000000..831df304e --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemMaterial.java @@ -0,0 +1,36 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.world.registry; + +public interface ItemMaterial { + /** + * Gets the the maximum quantity of this item that can be in a single stack. + * + * @return the maximum quantity + */ + int getMaxStackSize(); + + /** + * Gets the the maximum damage this item can take before being broken. + * + * @return the maximum damage, or 0 if not applicable + */ + int getMaxDamage(); +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemRegistry.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemRegistry.java index ae8c9d38e..4718e98ed 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemRegistry.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/ItemRegistry.java @@ -36,6 +36,15 @@ public interface ItemRegistry { @Nullable String getName(ItemType itemType); + /** + * Get the material for the given item. + * + * @param itemType the item + * @return the material, or null if the material information is not known + */ + @Nullable + ItemMaterial getMaterial(ItemType itemType); + /** * Register all items */ diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughBlockMaterial.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughBlockMaterial.java index 3edea1dd0..16ec7e53d 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughBlockMaterial.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughBlockMaterial.java @@ -21,21 +21,31 @@ package com.sk89q.worldedit.world.registry; import javax.annotation.Nullable; +import static com.sk89q.worldedit.util.GuavaUtil.firstNonNull; + public class PassthroughBlockMaterial implements BlockMaterial { - @Nullable private final BlockMaterial blockMaterial; + private static final SimpleBlockMaterial DEFAULT_MATERIAL = new SimpleBlockMaterial(); + + static { + DEFAULT_MATERIAL.setFullCube(true); + DEFAULT_MATERIAL.setOpaque(true); + DEFAULT_MATERIAL.setSolid(true); + DEFAULT_MATERIAL.setTicksRandomly(true); + DEFAULT_MATERIAL.setMovementBlocker(true); + DEFAULT_MATERIAL.setBurnable(true); + DEFAULT_MATERIAL.setToolRequired(true); + } + + private final BlockMaterial blockMaterial; public PassthroughBlockMaterial(@Nullable BlockMaterial material) { - this.blockMaterial = material; + this.blockMaterial = firstNonNull(material, DEFAULT_MATERIAL); } @Override public boolean isAir() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.isAir(); - } + return blockMaterial.isAir(); } @Override @@ -49,172 +59,96 @@ public class PassthroughBlockMaterial implements BlockMaterial { @Override public boolean isFullCube() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isFullCube(); - } + return blockMaterial.isFullCube(); } @Override public boolean isOpaque() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isOpaque(); - } + return blockMaterial.isOpaque(); } @Override public boolean isPowerSource() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.isPowerSource(); - } + return blockMaterial.isPowerSource(); } @Override public boolean isLiquid() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.isLiquid(); - } + return blockMaterial.isLiquid(); } @Override public boolean isSolid() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isSolid(); - } + return blockMaterial.isSolid(); } @Override public float getHardness() { - if (blockMaterial == null) { - return 0; - } else { - return blockMaterial.getHardness(); - } + return blockMaterial.getHardness(); } @Override public float getResistance() { - if (blockMaterial == null) { - return 0; - } else { - return blockMaterial.getResistance(); - } + return blockMaterial.getResistance(); } @Override public float getSlipperiness() { - if (blockMaterial == null) { - return 0; - } else { - return blockMaterial.getSlipperiness(); - } + return blockMaterial.getSlipperiness(); } @Override public int getLightValue() { - if (blockMaterial == null) { - return 0; - } else { - return blockMaterial.getLightValue(); - } + return blockMaterial.getLightValue(); } @Override public int getLightOpacity() { - if (blockMaterial == null) { - return 0; - } else { - return blockMaterial.getLightOpacity(); - } + return blockMaterial.getLightOpacity(); } @Override public boolean isFragileWhenPushed() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.isFragileWhenPushed(); - } + return blockMaterial.isFragileWhenPushed(); } @Override public boolean isUnpushable() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.isUnpushable(); - } + return blockMaterial.isUnpushable(); } @Override public boolean isTicksRandomly() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isTicksRandomly(); - } + return blockMaterial.isTicksRandomly(); } @Override public boolean isMovementBlocker() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isMovementBlocker(); - } + return blockMaterial.isMovementBlocker(); } @Override public boolean isBurnable() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isBurnable(); - } + return blockMaterial.isBurnable(); } @Override public boolean isToolRequired() { - if (blockMaterial == null) { - return true; - } else { - return blockMaterial.isToolRequired(); - } + return blockMaterial.isToolRequired(); } @Override public boolean isReplacedDuringPlacement() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.isReplacedDuringPlacement(); - } + return blockMaterial.isReplacedDuringPlacement(); } @Override public boolean isTranslucent() { - if (blockMaterial == null) { - return !isOpaque(); - } else { - return blockMaterial.isTranslucent(); - } + return blockMaterial.isTranslucent(); } @Override public boolean hasContainer() { - if (blockMaterial == null) { - return false; - } else { - return blockMaterial.hasContainer(); - } + return blockMaterial.hasContainer(); } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughItemMaterial.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughItemMaterial.java new file mode 100644 index 000000000..7912884cd --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/PassthroughItemMaterial.java @@ -0,0 +1,45 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.world.registry; + +import static com.sk89q.worldedit.util.GuavaUtil.firstNonNull; + +import javax.annotation.Nullable; + +public class PassthroughItemMaterial implements ItemMaterial { + + private static final ItemMaterial DEFAULT_MATERIAL = new SimpleItemMaterial(0, 0); + + private final ItemMaterial itemMaterial; + + public PassthroughItemMaterial(@Nullable ItemMaterial material) { + this.itemMaterial = firstNonNull(material, DEFAULT_MATERIAL); + } + + @Override + public int getMaxStackSize() { + return itemMaterial.getMaxStackSize(); + } + + @Override + public int getMaxDamage() { + return itemMaterial.getMaxDamage(); + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/SimpleMaterial.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/SimpleMaterial.java new file mode 100644 index 000000000..61c6b4f49 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/registry/SimpleMaterial.java @@ -0,0 +1,41 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.world.registry; + +class SimpleItemMaterial implements ItemMaterial { + + private int maxStackSize; + private int maxDamage; + + public SimpleItemMaterial(int maxStackSize, int maxDamage) { + this.maxStackSize = maxStackSize; + this.maxDamage = maxDamage; + } + + @Override + public int getMaxStackSize() { + return maxStackSize; + } + + @Override + public int getMaxDamage() { + return maxDamage; + } +} \ No newline at end of file diff --git a/worldedit-core/src/main/resources/lang/strings.json b/worldedit-core/src/main/resources/lang/strings.json index 1f5c2135a..60fc30309 100644 --- a/worldedit-core/src/main/resources/lang/strings.json +++ b/worldedit-core/src/main/resources/lang/strings.json @@ -397,7 +397,7 @@ "worldedit.undo.undone": "Undid {0} available edits.", "worldedit.undo.none": "Nothing left to undo.", - "worldedit.redo.undone": "Redid {0} available edits.", + "worldedit.redo.redone": "Redid {0} available edits.", "worldedit.redo.none": "Nothing left to redo.", "worldedit.clearhistory.cleared": "History cleared.", @@ -590,6 +590,7 @@ "worldedit.tool.inspect.equip": "Inspect tool bound to {0}.", "worldedit.tool.info.blockstate.hover": "Block state", "worldedit.tool.info.internalid.hover": "Internal ID", + "worldedit.tool.info.legacy.hover": "Legacy id:data", "worldedit.tool.info.light.hover": "Block Light/Light Above", "worldedit.tool.none.equip": "Tool unbound from your current item.", "worldedit.tool.selwand.equip": "Selection wand bound to {0}.", diff --git a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java index 91309130f..2ffa12c1d 100644 --- a/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java +++ b/worldedit-core/src/test/java/com/sk89q/worldedit/internal/expression/ExpressionTest.java @@ -91,7 +91,6 @@ class ExpressionTest extends BaseExpressionTest { { ExpressionException e = assertThrows(ExpressionException.class, () -> compile("rotate(1, 2, 3)")); - e.printStackTrace(); assertEquals(7, e.getPosition(), "Error position"); }