From 9d2630322e080a69d41de5f58ba12945eb4eed11 Mon Sep 17 00:00:00 2001 From: scdhh Date: Tue, 26 Aug 2025 14:22:22 +0800 Subject: [PATCH] Add built-in function and operators --- src/main/antlr/sfml/SFML.g4 | 28 ++- .../java/ca/teamdman/sfml/ast/ASTBuilder.java | 194 +++++++++++++++--- .../java/ca/teamdman/sfml/ast/BoolHas.java | 19 +- .../ca/teamdman/sfml/ast/BoolRedstone.java | 13 +- .../ca/teamdman/sfml/ast/FunctionArg.java | 48 +++++ .../ca/teamdman/sfml/ast/FunctionArgs.java | 6 + .../ca/teamdman/sfml/ast/FunctionHandler.java | 6 + .../teamdman/sfml/ast/FunctionRegistry.java | 55 +++++ .../ca/teamdman/sfml/ast/InputStatement.java | 4 +- .../java/ca/teamdman/sfml/ast/NumExpr.java | 9 + .../sfml/ast/NumFuncGetLabelCount.java | 21 ++ .../sfml/ast/NumFuncGetThingCount.java | 57 +++++ .../java/ca/teamdman/sfml/ast/Number.java | 7 +- .../ca/teamdman/sfml/ast/OutputStatement.java | 4 +- .../ca/teamdman/sfml/ast/ResourceLimit.java | 12 +- .../ca/teamdman/sfml/ast/ResourceLimits.java | 8 +- .../teamdman/sfml/ast/ResourceQuantity.java | 35 +++- .../teamdman/sfml/NumExprFunctionsTest.java | 107 ++++++++++ 18 files changed, 574 insertions(+), 59 deletions(-) create mode 100644 src/main/java/ca/teamdman/sfml/ast/FunctionArg.java create mode 100644 src/main/java/ca/teamdman/sfml/ast/FunctionArgs.java create mode 100644 src/main/java/ca/teamdman/sfml/ast/FunctionHandler.java create mode 100644 src/main/java/ca/teamdman/sfml/ast/FunctionRegistry.java create mode 100644 src/main/java/ca/teamdman/sfml/ast/NumExpr.java create mode 100644 src/main/java/ca/teamdman/sfml/ast/NumFuncGetLabelCount.java create mode 100644 src/main/java/ca/teamdman/sfml/ast/NumFuncGetThingCount.java create mode 100644 src/test/java/ca/teamdman/sfml/NumExprFunctionsTest.java diff --git a/src/main/antlr/sfml/SFML.g4 b/src/main/antlr/sfml/SFML.g4 index a9ff292c9..d43b864a1 100644 --- a/src/main/antlr/sfml/SFML.g4 +++ b/src/main/antlr/sfml/SFML.g4 @@ -54,8 +54,8 @@ limit : quantity retention #QuantityRetentionLimit | quantity #QuantityLimit ; -quantity : number EACH?; -retention : RETAIN number EACH?; +quantity : numexpr EACH?; +retention : RETAIN numexpr EACH?; resourceExclusion : EXCEPT resourceIdList; @@ -105,8 +105,23 @@ boolexpr : TRUE #BooleanTrue | NOT boolexpr #BooleanNegation | boolexpr AND boolexpr #BooleanConjunction | boolexpr OR boolexpr #BooleanDisjunction - | setOp? labelAccess HAS comparisonOp number resourceIdDisjunction? with? (EXCEPT resourceIdList)? #BooleanHas - | REDSTONE (comparisonOp number)? #BooleanRedstone + | setOp? labelAccess HAS comparisonOp numexpr resourceIdDisjunction? with? (EXCEPT resourceIdList)? #BooleanHas + | REDSTONE (comparisonOp numexpr)? #BooleanRedstone + ; + +numexpr : numterm ((PLUS | DASH) numterm)*; +numterm : numfactor ((STAR | SLASH) numfactor)*; +numfactor : number + | LPAREN numexpr RPAREN + | functionCall + ; + +// more flexible function args +functionCall : identifier LPAREN (functionArg (COMMA functionArg)*)? RPAREN; +functionArg : labelAccess + | resourceIdDisjunction + | numexpr + | string ; comparisonOp : GT @@ -142,7 +157,7 @@ label : (identifier) #RawLabel | string #StringLabel ; -identifier : (IDENTIFIER | REDSTONE | GLOBAL | SECOND | SECONDS) ; +identifier : (IDENTIFIER | STAR | REDSTONE | GLOBAL | SECOND | SECONDS) ; // GENERAL string: STRING ; @@ -244,13 +259,14 @@ COMMA : ','; COLON : ':'; SLASH : '/'; DASH : '-'; +STAR : '*'; LPAREN : '('; RPAREN : ')'; NUMBER_WITH_G_SUFFIX : [0-9]+[gG] ; NUMBER : [0-9]+ ; -IDENTIFIER : [a-zA-Z_*][a-zA-Z0-9_*]* | '*'; // Note that the * in the square brackets is a literl +IDENTIFIER : [a-zA-Z_*][a-zA-Z0-9_*]* ; STRING : '"' (~'"'|'\\"')* '"' ; diff --git a/src/main/java/ca/teamdman/sfml/ast/ASTBuilder.java b/src/main/java/ca/teamdman/sfml/ast/ASTBuilder.java index 816bdfc88..126c9a96a 100644 --- a/src/main/java/ca/teamdman/sfml/ast/ASTBuilder.java +++ b/src/main/java/ca/teamdman/sfml/ast/ASTBuilder.java @@ -22,7 +22,7 @@ public List> getNodesUnderCursor(int cursorPos) .stream() .filter(pair -> pair.getSecond() != null) .filter(pair -> pair.getSecond().start.getStartIndex() <= cursorPos - && pair.getSecond().stop.getStopIndex() >= cursorPos) + && pair.getSecond().stop.getStopIndex() >= cursorPos) .collect(Collectors.toList()); } @@ -112,8 +112,8 @@ public Label visitRawLabel(SFMLParser.RawLabelContext ctx) { if (label.name().length() > Program.MAX_LABEL_LENGTH) { throw new IllegalArgumentException( "Label name cannot be longer than " - + Program.MAX_LABEL_LENGTH - + " characters." + + Program.MAX_LABEL_LENGTH + + " characters." ); } USED_LABELS.add(label); @@ -127,8 +127,8 @@ public Label visitStringLabel(SFMLParser.StringLabelContext ctx) { if (label.name().length() > Program.MAX_LABEL_LENGTH) { throw new IllegalArgumentException( "Label name cannot be longer than " - + Program.MAX_LABEL_LENGTH - + " characters." + + Program.MAX_LABEL_LENGTH + + " characters." ); } USED_LABELS.add(label); @@ -166,8 +166,8 @@ public ASTNode visitTimerTrigger(SFMLParser.TimerTriggerContext ctx) { // get default min interval int minInterval = timerTrigger.usesOnlyForgeEnergyResourceIO() - ? SFMConfig.getOrDefault(SFMConfig.SERVER_CONFIG.timerTriggerMinimumIntervalInTicksWhenOnlyForgeEnergyIO) - : SFMConfig.getOrDefault(SFMConfig.SERVER_CONFIG.timerTriggerMinimumIntervalInTicks); + ? SFMConfig.getOrDefault(SFMConfig.SERVER_CONFIG.timerTriggerMinimumIntervalInTicksWhenOnlyForgeEnergyIO) + : SFMConfig.getOrDefault(SFMConfig.SERVER_CONFIG.timerTriggerMinimumIntervalInTicks); // validate interval if (time.ticks() < minInterval) { @@ -181,15 +181,12 @@ public ASTNode visitTimerTrigger(SFMLParser.TimerTriggerContext ctx) { @Override public ASTNode visitBooleanRedstone(SFMLParser.BooleanRedstoneContext ctx) { ComparisonOperator comp = ComparisonOperator.GREATER_OR_EQUAL; - Number num = new Number(0); - if (ctx.comparisonOp() != null && ctx.number() != null) { + NumExpr rhs = new Number(0); + if (ctx.comparisonOp() != null && ctx.numexpr() != null) { comp = visitComparisonOp(ctx.comparisonOp()); - num = visitNumber(ctx.number()); + rhs = visitNumexpr(ctx.numexpr()); } - if (num.value() > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Redstone signal strength cannot be greater than " + Integer.MAX_VALUE); - } - BoolExpr boolExpr = new BoolRedstone(comp, (int) num.value()); + BoolExpr boolExpr = new BoolRedstone(comp, rhs); AST_NODE_CONTEXTS.add(new Pair<>(boolExpr, ctx)); return boolExpr; } @@ -311,8 +308,8 @@ public LabelAccess visitLabelAccess(SFMLParser.LabelAccessContext ctx) { public RoundRobin visitRoundrobin(@Nullable SFMLParser.RoundrobinContext ctx) { if (ctx == null) return RoundRobin.disabled(); RoundRobin rtn = ctx.BLOCK() != null - ? new RoundRobin(RoundRobin.Behaviour.BY_BLOCK) - : new RoundRobin(RoundRobin.Behaviour.BY_LABEL); + ? new RoundRobin(RoundRobin.Behaviour.BY_BLOCK) + : new RoundRobin(RoundRobin.Behaviour.BY_LABEL); AST_NODE_CONTEXTS.add(new Pair<>(rtn, ctx)); return rtn; } @@ -365,7 +362,7 @@ public BoolExpr visitBooleanHas(SFMLParser.BooleanHasContext ctx) { var setOperator = visitSetOp(ctx.setOp()); var labelAccess = visitLabelAccess(ctx.labelAccess()); ComparisonOperator comparisonOperator = visitComparisonOp(ctx.comparisonOp()); - Number num = visitNumber(ctx.number()); + NumExpr num = visitNumexpr(ctx.numexpr()); ResourceIdSet resourceIdSet; if (ctx.resourceIdDisjunction() == null) { resourceIdSet = ResourceIdSet.MATCH_ALL; @@ -384,7 +381,7 @@ public BoolExpr visitBooleanHas(SFMLParser.BooleanHasContext ctx) { } else { except = visitResourceIdList(ctx.resourceIdList()); } - BoolHas rtn = new BoolHas(setOperator, labelAccess, comparisonOperator, num.value(), resourceIdSet, with, except); + BoolHas rtn = new BoolHas(setOperator, labelAccess, comparisonOperator, num, resourceIdSet, with, except); AST_NODE_CONTEXTS.add(new Pair<>(rtn, ctx)); return rtn; } @@ -704,10 +701,10 @@ public ResourceQuantity visitRetention(@Nullable SFMLParser.RetentionContext ctx if (ctx == null) return ResourceQuantity.UNSET; ResourceQuantity quantity = new ResourceQuantity( - visitNumber(ctx.number()), + visitNumexpr(ctx.numexpr()), ctx.EACH() != null - ? ResourceQuantity.IdExpansionBehaviour.EXPAND - : ResourceQuantity.IdExpansionBehaviour.NO_EXPAND + ? ResourceQuantity.IdExpansionBehaviour.EXPAND + : ResourceQuantity.IdExpansionBehaviour.NO_EXPAND ); AST_NODE_CONTEXTS.add(new Pair<>(quantity, ctx)); return quantity; @@ -717,10 +714,10 @@ public ResourceQuantity visitRetention(@Nullable SFMLParser.RetentionContext ctx public ResourceQuantity visitQuantity(@Nullable SFMLParser.QuantityContext ctx) { if (ctx == null) return ResourceQuantity.MAX_QUANTITY; ResourceQuantity quantity = new ResourceQuantity( - visitNumber(ctx.number()), + visitNumexpr(ctx.numexpr()), ctx.EACH() != null - ? ResourceQuantity.IdExpansionBehaviour.EXPAND - : ResourceQuantity.IdExpansionBehaviour.NO_EXPAND + ? ResourceQuantity.IdExpansionBehaviour.EXPAND + : ResourceQuantity.IdExpansionBehaviour.NO_EXPAND ); AST_NODE_CONTEXTS.add(new Pair<>(quantity, ctx)); return quantity; @@ -767,4 +764,153 @@ public Block visitBlock(@Nullable SFMLParser.BlockContext ctx) { AST_NODE_CONTEXTS.add(new Pair<>(block, ctx)); return block; } + + // --- Numeric expressions --- + public NumExpr visitNumexpr(SFMLParser.NumexprContext ctx) { + // fold left: numterm ((+|-) numterm)* + NumExpr acc = visitNumterm(ctx.numterm(0)); + int childCount = ctx.getChildCount(); + NumExpr currentRight; + Integer pendingOp = null; // token type: PLUS or DASH + for (int i = 0; i < childCount; i++) { + ParseTree child = ctx.getChild(i); + if (child instanceof SFMLParser.NumtermContext termCtx) { + if (pendingOp != null) { + currentRight = visitNumterm(termCtx); + NumExpr lhs = acc; + NumExpr rhs = currentRight; + if (pendingOp == SFMLParser.PLUS) { + acc = new NumExpr() { + final NumExpr L = lhs, R = rhs; + + @Override + public long eval(ca.teamdman.sfm.common.program.ProgramContext context) { + return L.eval(context) + R.eval(context); + } + + @Override + public String toString() { + return "(" + L + " + " + R + ")"; + } + }; + } else { + acc = new NumExpr() { + final NumExpr L = lhs, R = rhs; + + @Override + public long eval(ca.teamdman.sfm.common.program.ProgramContext context) { + return L.eval(context) - R.eval(context); + } + + @Override + public String toString() { + return "(" + L + " - " + R + ")"; + } + }; + } + pendingOp = null; + } + } else if (child instanceof TerminalNode tn) { + int t = tn.getSymbol().getType(); + if (t == SFMLParser.PLUS || t == SFMLParser.DASH) { + pendingOp = t; + } + } + } + AST_NODE_CONTEXTS.add(new Pair<>(acc, ctx)); + return acc; + } + + public NumExpr visitNumterm(SFMLParser.NumtermContext ctx) { + // fold left: numfactor ((*|/) numfactor)* + NumExpr acc = visitNumfactor(ctx.numfactor(0)); + int childCount = ctx.getChildCount(); + NumExpr currentRight; + Integer pendingOp = null; // token: STAR or SLASH + for (int i = 0; i < childCount; i++) { + ParseTree child = ctx.getChild(i); + if (child instanceof SFMLParser.NumfactorContext fctx) { + if (pendingOp != null) { + currentRight = visitNumfactor(fctx); + NumExpr lhs = acc; + NumExpr rhs = currentRight; + if (pendingOp == SFMLParser.STAR) { + acc = new NumExpr() { + final NumExpr L = lhs, R = rhs; + + @Override + public long eval(ca.teamdman.sfm.common.program.ProgramContext context) { + return L.eval(context) * R.eval(context); + } + + @Override + public String toString() { + return "(" + L + " * " + R + ")"; + } + }; + } else { + acc = new NumExpr() { + final NumExpr L = lhs, R = rhs; + + @Override + public long eval(ca.teamdman.sfm.common.program.ProgramContext context) { + long d = R.eval(context); + return d == 0 ? 0 : L.eval(context) / d; + } + + @Override + public String toString() { + return "(" + L + " / " + R + ")"; + } + }; + } + pendingOp = null; + } + } else if (child instanceof TerminalNode tn) { + int t = tn.getSymbol().getType(); + if (t == SFMLParser.STAR || t == SFMLParser.SLASH) { + pendingOp = t; + } + } + } + AST_NODE_CONTEXTS.add(new Pair<>(acc, ctx)); + return acc; + } + + public NumExpr visitNumfactor(SFMLParser.NumfactorContext ctx) { + if (ctx.number() != null) { + return visitNumber(ctx.number()); + } + if (ctx.functionCall() != null) { + return visitFunctionCall(ctx.functionCall()); + } + // parenthesized + NumExpr inner = visitNumexpr(ctx.numexpr()); + AST_NODE_CONTEXTS.add(new Pair<>(inner, ctx)); + return inner; + } + + public NumExpr visitFunctionCall(SFMLParser.FunctionCallContext ctx) { + String fname = ctx.identifier().getText().toLowerCase(Locale.ROOT); + List ordered = new ArrayList<>(); + if (ctx.functionArg() != null) { + for (SFMLParser.FunctionArgContext arg : ctx.functionArg()) { + if (arg.labelAccess() != null) { + ordered.add(FunctionArg.ofLabel(visitLabelAccess(arg.labelAccess()))); + } else if (arg.resourceIdDisjunction() != null) { + ordered.add(FunctionArg.ofResourceIds(visitResourceIdDisjunction(arg.resourceIdDisjunction()))); + } else if (arg.numexpr() != null) { + ordered.add(FunctionArg.ofNumExpr(visitNumexpr(arg.numexpr()))); + } else if (arg.string() != null) { + ordered.add(FunctionArg.ofString(visitString(arg.string()))); + } else { + throw new IllegalArgumentException("Unrecognized function argument in function: " + fname); + } + } + } + FunctionHandler handler = FunctionRegistry.get(fname); + NumExpr result = handler.build(new FunctionArgs(ordered)); + AST_NODE_CONTEXTS.add(new Pair<>(result, ctx)); + return result; + } } diff --git a/src/main/java/ca/teamdman/sfml/ast/BoolHas.java b/src/main/java/ca/teamdman/sfml/ast/BoolHas.java index 09b578a89..d0dc50a56 100644 --- a/src/main/java/ca/teamdman/sfml/ast/BoolHas.java +++ b/src/main/java/ca/teamdman/sfml/ast/BoolHas.java @@ -14,14 +14,27 @@ public record BoolHas( SetOperator setOperator, LabelAccess labelAccess, ComparisonOperator comparisonOperator, - long quantity, + NumExpr quantity, ResourceIdSet resourceIdSet, With with, ResourceIdSet except ) implements BoolExpr { + public BoolHas( + SetOperator setOperator, + LabelAccess labelAccess, + ComparisonOperator comparisonOperator, + long quantity, + ResourceIdSet resourceIdSet, + With with, + ResourceIdSet except + ) { + this(setOperator, labelAccess, comparisonOperator, new Number(quantity), resourceIdSet, with, except); + } + @Override public boolean test(ProgramContext programContext) { + long threshold = this.quantity.eval(programContext); AtomicLong overallCount = new AtomicLong(0); List satisfactionResults = new ArrayList<>(); LabelPositionHolder labelPositionHolder = programContext.getLabelPositionHolder(); @@ -38,10 +51,10 @@ public boolean test(ProgramContext programContext) { resourceType ); } - satisfactionResults.add(comparisonOperator.test(inThisInv.get(), quantity)); + satisfactionResults.add(comparisonOperator.test(inThisInv.get(), threshold)); } - var isOverallSatisfied = this.comparisonOperator.test(overallCount.get(), this.quantity); + var isOverallSatisfied = this.comparisonOperator.test(overallCount.get(), threshold); return setOperator.test(isOverallSatisfied, satisfactionResults); } diff --git a/src/main/java/ca/teamdman/sfml/ast/BoolRedstone.java b/src/main/java/ca/teamdman/sfml/ast/BoolRedstone.java index 1d68bdcd6..d1e02a07b 100644 --- a/src/main/java/ca/teamdman/sfml/ast/BoolRedstone.java +++ b/src/main/java/ca/teamdman/sfml/ast/BoolRedstone.java @@ -4,20 +4,23 @@ import ca.teamdman.sfm.common.program.ProgramContext; import net.minecraft.world.level.Level; -public record BoolRedstone(ComparisonOperator operator, long number) implements BoolExpr { - @SuppressWarnings("UnnecessaryLocalVariable") +public record BoolRedstone(ComparisonOperator operator, NumExpr rhs) implements BoolExpr { + public BoolRedstone(ComparisonOperator operator, long number) { + this(operator, new Number(number)); + } + @Override public boolean test(ProgramContext programContext) { ManagerBlockEntity manager = programContext.getManager(); Level level = manager.getLevel(); assert level != null; long lhs = level.getBestNeighborSignal(manager.getBlockPos()); - long rhs = number; - return operator.test(lhs, rhs); + long rhsVal = rhs.eval(programContext); + return operator.test(lhs, rhsVal); } @Override public String toString() { - return "REDSTONE " + operator + " " + number; + return "REDSTONE " + operator + " " + rhs; } } diff --git a/src/main/java/ca/teamdman/sfml/ast/FunctionArg.java b/src/main/java/ca/teamdman/sfml/ast/FunctionArg.java new file mode 100644 index 000000000..b1bd6fd89 --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/FunctionArg.java @@ -0,0 +1,48 @@ +package ca.teamdman.sfml.ast; + +public final class FunctionArg { + private final Kind kind; + private final Object value; + private FunctionArg(Kind kind, Object value) { + this.kind = kind; + this.value = value; + } + + public static FunctionArg ofLabel(LabelAccess labelAccess) { + return new FunctionArg(Kind.LABEL_ACCESS, labelAccess); + } + + public static FunctionArg ofResourceIds(ResourceIdSet resourceIdSet) { + return new FunctionArg(Kind.RESOURCE_IDS, resourceIdSet); + } + + public static FunctionArg ofNumExpr(NumExpr numExpr) { + return new FunctionArg(Kind.NUM, numExpr); + } + + public static FunctionArg ofString(StringHolder str) { + return new FunctionArg(Kind.STRING, str); + } + + public Kind kind() { + return kind; + } + + public LabelAccess asLabelAccess() { + return (LabelAccess) value; + } + + public ResourceIdSet asResourceIds() { + return (ResourceIdSet) value; + } + + public NumExpr asNumExpr() { + return (NumExpr) value; + } + + public StringHolder asString() { + return (StringHolder) value; + } + + public enum Kind {LABEL_ACCESS, RESOURCE_IDS, NUM, STRING} +} diff --git a/src/main/java/ca/teamdman/sfml/ast/FunctionArgs.java b/src/main/java/ca/teamdman/sfml/ast/FunctionArgs.java new file mode 100644 index 000000000..f5e3b1e51 --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/FunctionArgs.java @@ -0,0 +1,6 @@ +package ca.teamdman.sfml.ast; + +import java.util.List; + +public record FunctionArgs(List args) { +} diff --git a/src/main/java/ca/teamdman/sfml/ast/FunctionHandler.java b/src/main/java/ca/teamdman/sfml/ast/FunctionHandler.java new file mode 100644 index 000000000..9caa71ef6 --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/FunctionHandler.java @@ -0,0 +1,6 @@ +package ca.teamdman.sfml.ast; + +@FunctionalInterface +public interface FunctionHandler { + NumExpr build(FunctionArgs args); +} diff --git a/src/main/java/ca/teamdman/sfml/ast/FunctionRegistry.java b/src/main/java/ca/teamdman/sfml/ast/FunctionRegistry.java new file mode 100644 index 000000000..02daba6bc --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/FunctionRegistry.java @@ -0,0 +1,55 @@ +package ca.teamdman.sfml.ast; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public final class FunctionRegistry { + private static final Map HANDLERS = new HashMap<>(); + + static { + // get_label_count(labelAccess) + register("get_label_count", args -> { + List arg = args.args(); + if (arg.size() != 1 || arg.get(0).kind() != FunctionArg.Kind.LABEL_ACCESS) { + throw new IllegalArgumentException("get_label_count expects exactly 1 argument: labelAccess"); + } + return new NumFuncGetLabelCount(arg.get(0).asLabelAccess()); + }); + + // get_thing_count(labelAccess[, resourceIds]) + register("get_thing_count", args -> { + List arg = args.args(); + if (arg.isEmpty()) { + throw new IllegalArgumentException("get_thing_count expects 1 or 2 arguments: labelAccess[, resourceIds]"); + } + if (arg.get(0).kind() != FunctionArg.Kind.LABEL_ACCESS) { + throw new IllegalArgumentException("get_thing_count first argument must be labelAccess"); + } + LabelAccess la = arg.get(0).asLabelAccess(); + ResourceIdSet ids = ResourceIdSet.MATCH_ALL; + if (arg.size() >= 2) { + if (arg.get(1).kind() != FunctionArg.Kind.RESOURCE_IDS) { + throw new IllegalArgumentException("get_thing_count second argument must be resource ids if provided"); + } + ids = arg.get(1).asResourceIds(); + } + if (arg.size() > 2) { + throw new IllegalArgumentException("get_thing_count accepts at most 2 arguments"); + } + return new NumFuncGetThingCount(la, ids); + }); + } + + private FunctionRegistry() { + } + + public static void register(String name, FunctionHandler handler) { + HANDLERS.put(name.toLowerCase(Locale.ROOT), handler); + } + + public static FunctionHandler get(String name) { + return HANDLERS.get(name.toLowerCase(Locale.ROOT)); + } +} diff --git a/src/main/java/ca/teamdman/sfml/ast/InputStatement.java b/src/main/java/ca/teamdman/sfml/ast/InputStatement.java index ba00f515f..00f4fffe6 100644 --- a/src/main/java/ca/teamdman/sfml/ast/InputStatement.java +++ b/src/main/java/ca/teamdman/sfml/ast/InputStatement.java @@ -84,7 +84,7 @@ public void gatherSlots( context.getLogger().debug(x -> x.accept(LOG_PROGRAM_TICK_IO_STATEMENT_GATHER_SLOTS_NOT_EACH.get())); // create a single matcher to be shared by all capabilities - List inputTrackers = resourceLimits.createInputTrackers(); + List inputTrackers = resourceLimits.createInputTrackers(context); for (var resourceType : resourceLimits.getReferencedResourceTypes()) { // TODO: Fix #166 // log gather for resource type context @@ -121,7 +121,7 @@ public void gatherSlots( // gather slots for each capability found for positions tagged by a provided label Consumer> finalSlotConsumer = slotConsumer; resourceType.forEachCapability(context, labelAccess, (label, pos, direction, cap) -> { - List inputTrackers = resourceLimits.createInputTrackers(); + List inputTrackers = resourceLimits.createInputTrackers(context); gatherSlotsForCap( context, (ResourceType) resourceType, diff --git a/src/main/java/ca/teamdman/sfml/ast/NumExpr.java b/src/main/java/ca/teamdman/sfml/ast/NumExpr.java new file mode 100644 index 000000000..80ea98a18 --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/NumExpr.java @@ -0,0 +1,9 @@ +package ca.teamdman.sfml.ast; + +import ca.teamdman.sfm.common.program.ProgramContext; + +public interface NumExpr extends ASTNode { + long eval(ProgramContext context); + + String toString(); +} diff --git a/src/main/java/ca/teamdman/sfml/ast/NumFuncGetLabelCount.java b/src/main/java/ca/teamdman/sfml/ast/NumFuncGetLabelCount.java new file mode 100644 index 000000000..09d1de570 --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/NumFuncGetLabelCount.java @@ -0,0 +1,21 @@ +package ca.teamdman.sfml.ast; + +import ca.teamdman.sfm.common.program.ProgramContext; + +public final class NumFuncGetLabelCount implements NumExpr { + private final LabelAccess labelAccess; + + public NumFuncGetLabelCount(LabelAccess labelAccess) { + this.labelAccess = labelAccess; + } + + @Override + public long eval(ProgramContext context) { + return labelAccess.getLabelledPositions(context.getLabelPositionHolder()).size(); + } + + @Override + public String toString() { + return "get_label_count(" + labelAccess + ")"; + } +} diff --git a/src/main/java/ca/teamdman/sfml/ast/NumFuncGetThingCount.java b/src/main/java/ca/teamdman/sfml/ast/NumFuncGetThingCount.java new file mode 100644 index 000000000..f112f886d --- /dev/null +++ b/src/main/java/ca/teamdman/sfml/ast/NumFuncGetThingCount.java @@ -0,0 +1,57 @@ +package ca.teamdman.sfml.ast; + +import ca.teamdman.sfm.common.program.ProgramContext; +import ca.teamdman.sfm.common.resourcetype.ResourceType; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.BlockPos; + +import java.util.concurrent.atomic.AtomicLong; + +public final class NumFuncGetThingCount implements NumExpr { + private final LabelAccess labelAccess; + private final ResourceIdSet resourceIds; // optional; empty => match all + + public NumFuncGetThingCount(LabelAccess labelAccess, ResourceIdSet resourceIds) { + this.labelAccess = labelAccess; + this.resourceIds = resourceIds; + } + + @Override + public long eval(ProgramContext context) { + AtomicLong total = new AtomicLong(0); + var positions = labelAccess.getLabelledPositions(context.getLabelPositionHolder()); + for (Pair entry : positions) { + BlockPos pos = entry.getSecond(); + for (ResourceType resourceType : resourceIds.getReferencedResourceTypes()) { + accumulate(context, pos, total, resourceType); + } + } + return total.get(); + } + + private void accumulate( + ProgramContext programContext, + BlockPos pos, + AtomicLong accumulator, + ResourceType resourceType + ) { + resourceType.forEachDirectionalCapability( + programContext, + labelAccess.directions(), + pos, + (direction, cap) -> resourceType.getStacksInSlots(cap, labelAccess.slots()).forEach(stack -> { + if (this.resourceIds.getMatchingFromStack(stack) != null) { + accumulator.addAndGet(resourceType.getAmount(stack)); + } + }) + ); + } + + @Override + public String toString() { + if (resourceIds.isEmpty()) { + return "get_thing_count(" + labelAccess + ")"; + } + return "get_thing_count(" + labelAccess + ", " + resourceIds.toStringCondensed() + ")"; + } +} diff --git a/src/main/java/ca/teamdman/sfml/ast/Number.java b/src/main/java/ca/teamdman/sfml/ast/Number.java index beda2faa0..62408d1d6 100644 --- a/src/main/java/ca/teamdman/sfml/ast/Number.java +++ b/src/main/java/ca/teamdman/sfml/ast/Number.java @@ -1,6 +1,6 @@ package ca.teamdman.sfml.ast; -public record Number(long value) implements ASTNode { +public record Number(long value) implements ASTNode, NumExpr { @Override public String toString() { return String.valueOf(value); @@ -9,4 +9,9 @@ public String toString() { public Number add(Number number) { return new Number(value + number.value); } + + @Override + public long eval(ca.teamdman.sfm.common.program.ProgramContext context) { + return value; + } } diff --git a/src/main/java/ca/teamdman/sfml/ast/OutputStatement.java b/src/main/java/ca/teamdman/sfml/ast/OutputStatement.java index 0592a5d21..f186ef782 100644 --- a/src/main/java/ca/teamdman/sfml/ast/OutputStatement.java +++ b/src/main/java/ca/teamdman/sfml/ast/OutputStatement.java @@ -433,7 +433,7 @@ public void gatherSlots( if (!each) { context.getLogger().debug(x -> x.accept(LOG_PROGRAM_TICK_IO_STATEMENT_GATHER_SLOTS_NOT_EACH.get())); // create a single list of trackers to be shared between all limited slots - List outputTracker = resourceLimits.createOutputTrackers(); + List outputTracker = resourceLimits.createOutputTrackers(context); for (var resourceType : resourceLimits.getReferencedResourceTypes()) { context .getLogger() @@ -465,7 +465,7 @@ public void gatherSlots( ))); resourceType.forEachCapability(context, labelAccess, (label, pos, direction, cap) -> { // create a new list of trackers for each limited slot - List outputTracker = resourceLimits.createOutputTrackers(); + List outputTracker = resourceLimits.createOutputTrackers(context); gatherSlotsForCap( context, (ResourceType) resourceType, diff --git a/src/main/java/ca/teamdman/sfml/ast/ResourceLimit.java b/src/main/java/ca/teamdman/sfml/ast/ResourceLimit.java index d77d20851..59d65755b 100644 --- a/src/main/java/ca/teamdman/sfml/ast/ResourceLimit.java +++ b/src/main/java/ca/teamdman/sfml/ast/ResourceLimit.java @@ -27,6 +27,14 @@ public ResourceLimit withLimit(Limit limit) { return new ResourceLimit(resourceIds, limit, with); } + public ResourceLimit withEvaluated(ca.teamdman.sfm.common.program.ProgramContext context) { + ResourceQuantity quantityExpr = limit.quantity(); + ResourceQuantity retentionExpr = limit.retention(); + ResourceQuantity quantityValue = quantityExpr == ResourceQuantity.UNSET ? ResourceQuantity.UNSET : new ResourceQuantity(new Number(quantityExpr.eval(context)), quantityExpr.idExpansionBehaviour()); + ResourceQuantity retentionValue = retentionExpr == ResourceQuantity.UNSET ? ResourceQuantity.UNSET : new ResourceQuantity(new Number(retentionExpr.eval(context)), retentionExpr.idExpansionBehaviour()); + return new ResourceLimit(resourceIds, new Limit(quantityValue, retentionValue), with); + } + public IInputResourceTracker createInputTracker( ResourceIdSet exclusions ) { @@ -79,8 +87,8 @@ public String toStringCondensed(Limit defaults) { return ( limit.toStringCondensed(defaults) + " " + resourceIds.toStringCondensed() + ( with == With.ALWAYS_TRUE - ? "" - : " WITH " + with + ? "" + : " WITH " + with ) ).trim(); } diff --git a/src/main/java/ca/teamdman/sfml/ast/ResourceLimits.java b/src/main/java/ca/teamdman/sfml/ast/ResourceLimits.java index 42ebfe1c6..bd72fe087 100644 --- a/src/main/java/ca/teamdman/sfml/ast/ResourceLimits.java +++ b/src/main/java/ca/teamdman/sfml/ast/ResourceLimits.java @@ -34,18 +34,18 @@ public ResourceLimits( this.exclusions = exclusions; } - public List createInputTrackers() { + public List createInputTrackers(ca.teamdman.sfm.common.program.ProgramContext context) { List rtn = new ObjectArrayList<>(resourceLimitList.size()); for (ResourceLimit rl : resourceLimitList) { - rtn.add(rl.createInputTracker(exclusions)); + rtn.add(rl.withEvaluated(context).createInputTracker(exclusions)); } return rtn; } - public List createOutputTrackers() { + public List createOutputTrackers(ca.teamdman.sfm.common.program.ProgramContext context) { List rtn = new ObjectArrayList<>(resourceLimitList.size()); for (ResourceLimit rl : resourceLimitList) { - rtn.add(rl.createOutputTracker(exclusions)); + rtn.add(rl.withEvaluated(context).createOutputTracker(exclusions)); } return rtn; } diff --git a/src/main/java/ca/teamdman/sfml/ast/ResourceQuantity.java b/src/main/java/ca/teamdman/sfml/ast/ResourceQuantity.java index 8c9c6ced9..c90d564e8 100644 --- a/src/main/java/ca/teamdman/sfml/ast/ResourceQuantity.java +++ b/src/main/java/ca/teamdman/sfml/ast/ResourceQuantity.java @@ -1,7 +1,9 @@ package ca.teamdman.sfml.ast; +import ca.teamdman.sfm.common.program.ProgramContext; + public record ResourceQuantity( - Number number, + NumExpr expr, IdExpansionBehaviour idExpansionBehaviour ) implements ASTNode { @SuppressWarnings("DataFlowIssue") @@ -11,20 +13,33 @@ public record ResourceQuantity( IdExpansionBehaviour.NO_EXPAND ); - public ResourceQuantity add(ResourceQuantity quantity) { - return new ResourceQuantity( - number.add(quantity.number), - idExpansionBehaviour - ); + public long eval(ProgramContext context) { + if (this == UNSET) return 0; + return expr.eval(context); } - public enum IdExpansionBehaviour { - EXPAND, - NO_EXPAND + public ResourceQuantity add(ResourceQuantity quantity) { + // simple add only when both sides are Number literals; else fallback to rhs + if (this.expr instanceof Number a && quantity.expr instanceof Number b) { + return new ResourceQuantity( + new Number(a.value() + b.value()), + idExpansionBehaviour + ); + } + return quantity; // minimal impl; not used in critical paths } @Override public String toString() { - return (this == UNSET ? "UNSET" : number) + (idExpansionBehaviour == IdExpansionBehaviour.EXPAND ? " EACH" : ""); + return (this == UNSET ? "UNSET" : String.valueOf(expr)) + (idExpansionBehaviour == IdExpansionBehaviour.EXPAND ? " EACH" : ""); + } + + public Number number() { + return expr instanceof Number n ? n : new Number(0); + } + + public enum IdExpansionBehaviour { + EXPAND, + NO_EXPAND } } diff --git a/src/test/java/ca/teamdman/sfml/NumExprFunctionsTest.java b/src/test/java/ca/teamdman/sfml/NumExprFunctionsTest.java new file mode 100644 index 000000000..a5c242f4f --- /dev/null +++ b/src/test/java/ca/teamdman/sfml/NumExprFunctionsTest.java @@ -0,0 +1,107 @@ +package ca.teamdman.sfml; + +import ca.teamdman.sfml.program_builder.ProgramBuildResult; +import ca.teamdman.sfml.program_builder.ProgramBuilder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +public class NumExprFunctionsTest { + + private void assertBuilds(String program) { + ProgramBuildResult result = ProgramBuilder.build(program); + if (!result.isBuildSuccessful()) { + StringBuilder sb = new StringBuilder(); + result.metadata().errors().forEach(e -> sb.append(e.getKey()).append("\n")); + // extra diagnostics + var ce = SFMLTestHelpers.getCompileErrors(program); + sb.append("Lexer errors:\n"); + ce.lexerErrors().forEach(s -> sb.append(s).append("\n")); + sb.append("Parser errors:\n"); + ce.parserErrors().forEach(s -> sb.append(s).append("\n")); + sb.append("Visit problems:\n"); + ce.visitProblems().forEach(t -> sb.append(t.getClass().getSimpleName()).append(": ").append(t.getMessage()).append("\n")); + fail("Program failed to build. Errors:\n" + sb); + } + } + + @Test + public void parses_get_label_count_in_has_condition() { + String prog = """ + NAME "test" + EVERY 20 TICKS DO + IF OVERALL a HAS >= get_label_count(a) THEN + END + END + """; + assertBuilds(prog); + } + + @Test + public void parses_get_thing_count_with_matchers_and_math() { + String prog = """ + NAME "test2" + EVERY 20 TICKS DO + IF SOME a HAS > (get_thing_count(a, minecraft:iron_ingot OR minecraft:gold_ingot) + 2 * 3) THEN + END + END + """; + assertBuilds(prog); + } + + @Test + public void parses_redstone_with_numexpr_and_function() { + String prog = """ + NAME "test3" + EVERY 20 TICKS DO + IF REDSTONE >= get_label_count(a) + 5 THEN + END + END + """; + assertBuilds(prog); + } + + @Test + public void parses_output_limit_with_functions_and_ops() { + String prog = """ + NAME "io-test-out" + EVERY 20 TICKS DO + OUTPUT get_thing_count(a) / (get_label_count(b) + 1) TO c + END + """; + assertBuilds(prog); + } + + @Test + public void parses_input_limit_with_functions_and_ops() { + String prog = """ + NAME "io-test-in" + EVERY 20 TICKS DO + INPUT get_thing_count(a, minecraft:iron_ingot) + 2 FROM b + END + """; + assertBuilds(prog); + } + + @Test + public void parses_output_limit_with_function_only() { + String prog = """ + NAME "io-test-out2" + EVERY 20 TICKS DO + OUTPUT get_label_count(a) TO c + END + """; + assertBuilds(prog); + } + + @Test + public void parses_input_limit_with_function_only() { + String prog = """ + NAME "io-test-in2" + EVERY 20 TICKS DO + INPUT get_label_count(a) FROM b + END + """; + assertBuilds(prog); + } +}