diff --git a/rulesengine/build.gradle.kts b/rulesengine/build.gradle.kts index 4d0eb751f..34e56473c 100644 --- a/rulesengine/build.gradle.kts +++ b/rulesengine/build.gradle.kts @@ -1,5 +1,6 @@ plugins { id("smithy-java.module-conventions") + id("smithy-java.jmh-conventions") } description = "Implements the rules engine traits used to resolve endpoints" diff --git a/rulesengine/src/jmh/java/software/amazon/smithy/java/rulesengine/TemplateResolutionBenchmark.java b/rulesengine/src/jmh/java/software/amazon/smithy/java/rulesengine/TemplateResolutionBenchmark.java new file mode 100644 index 000000000..9bbcc53e6 --- /dev/null +++ b/rulesengine/src/jmh/java/software/amazon/smithy/java/rulesengine/TemplateResolutionBenchmark.java @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.rulesengine; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import software.amazon.smithy.java.context.Context; + +@State(Scope.Thread) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 10, time = 1) +public class TemplateResolutionBenchmark { + + @Param({"3", "5", "9"}) + public int segmentCount; + + private BytecodeEvaluator resolveRegisters; + private BytecodeEvaluator buildRegisters; + private BytecodeEvaluator resolveProperties; + private BytecodeEvaluator buildProperties; + + @Setup(Level.Trial) + public void setup() { + resolveRegisters = createEvaluator(createProgram(false, false)); + buildRegisters = createEvaluator(createProgram(true, false)); + resolveProperties = createEvaluator(createProgram(false, true)); + buildProperties = createEvaluator(createProgram(true, true)); + } + + @Benchmark + public boolean resolveRegisters() { + return resolveRegisters.test(0); + } + + @Benchmark + public boolean buildRegisters() { + return buildRegisters.test(0); + } + + @Benchmark + public boolean resolveProperties() { + return resolveProperties.test(0); + } + + @Benchmark + public boolean buildProperties() { + return buildProperties.test(0); + } + + private Bytecode createProgram(boolean buildTemplate, boolean properties) { + BytecodeWriter writer = new BytecodeWriter(); + int dynamicCount = segmentCount / 2; + RegisterDefinition[] registers = new RegisterDefinition[dynamicCount]; + StringBuilder expected = new StringBuilder(); + + writer.markConditionStart(); + if (buildTemplate) { + writer.writeByte(Opcodes.BUILD_TEMPLATE); + writer.writeByte(segmentCount); + } + + for (int i = 0; i < segmentCount; i++) { + if ((i & 1) == 0) { + String literal = i == 0 ? "service." : "."; + expected.append(literal); + writeLiteral(writer, buildTemplate, literal); + } else { + int register = i / 2; + String value = "value" + register; + expected.append(value); + registers[register] = new RegisterDefinition( + "register" + register, + false, + properties ? Map.of("value", value) : value, + null, + false); + writeDynamic(writer, buildTemplate, properties, register); + } + } + + if (!buildTemplate) { + writer.writeByte(Opcodes.RESOLVE_TEMPLATE); + writer.writeByte(segmentCount); + } + writeLoadConstant(writer, writer.getConstantIndex(expected.toString())); + writer.writeByte(Opcodes.STRING_EQUALS); + writer.writeByte(Opcodes.RETURN_VALUE); + + return writer.build(registers, new RulesFunction[0], new int[] {-1, 1, -1}, 1); + } + + private void writeLiteral(BytecodeWriter writer, boolean buildTemplate, String literal) { + int constant = writer.getConstantIndex(literal); + if (buildTemplate) { + writer.writeByte(TemplateSegmentType.LITERAL); + writer.writeShort(constant); + } else { + writeLoadConstant(writer, constant); + } + } + + private void writeDynamic(BytecodeWriter writer, boolean buildTemplate, boolean property, int register) { + if (buildTemplate) { + writer.writeByte(property ? TemplateSegmentType.REGISTER_PROPERTY : TemplateSegmentType.REGISTER); + writer.writeByte(register); + if (property) { + writer.writeShort(writer.getConstantIndex("value")); + } + } else if (property) { + writer.writeByte(Opcodes.GET_PROPERTY_REG); + writer.writeByte(register); + writer.writeShort(writer.getConstantIndex("value")); + } else { + writer.writeByte(Opcodes.LOAD_REGISTER); + writer.writeByte(register); + } + } + + private void writeLoadConstant(BytecodeWriter writer, int constant) { + if (constant < 256) { + writer.writeByte(Opcodes.LOAD_CONST); + writer.writeByte(constant); + } else { + writer.writeByte(Opcodes.LOAD_CONST_W); + writer.writeShort(constant); + } + } + + private BytecodeEvaluator createEvaluator(Bytecode bytecode) { + RegisterFiller filler = RegisterFiller.of(bytecode, Collections.emptyMap()); + BytecodeEvaluator evaluator = new BytecodeEvaluator(bytecode, new RulesExtension[0], filler); + evaluator.reset(Context.empty(), Collections.emptyMap()); + return evaluator; + } +} diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Bytecode.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Bytecode.java index baeb38b11..d0c27181c 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Bytecode.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Bytecode.java @@ -31,7 +31,7 @@ * Offset Size Description * ------ ---- ----------- * 0 4 Magic number (0x52554C45 = "RULE") - * 4 2 Version (rolling version number, currently 1) + * 4 2 Version (rolling version number, currently 2) * 6 2 Condition count (unsigned short) * 8 2 Result count (unsigned short) * 10 2 Register count (unsigned short) @@ -153,7 +153,7 @@ public final class Bytecode { static final int MAGIC = 0x52554C45; // "RULE" - static final short VERSION = 1; + static final short VERSION = 2; static final byte CONST_NULL = 0; static final byte CONST_STRING = 1; static final byte CONST_INTEGER = 2; diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java index 801476fcd..a670604c8 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeCompiler.java @@ -39,6 +39,8 @@ final class BytecodeCompiler { + private static final int MIN_BUILD_TEMPLATE_SEGMENTS = 3; + private final List extensions; private final EndpointBddTrait bdd; private final Map> builtinProviders; @@ -204,48 +206,12 @@ private void compileEndpointUrl(Expression urlExpression) { } } - // Compile host parts: afterScheme + parts[1..pathPartIndex) - int hostPartCount = 0; - if (!afterScheme.isEmpty()) { - addLoadConst(afterScheme); - hostPartCount++; - } int hostEnd = pathPartIndex > 0 ? pathPartIndex : parts.size(); - for (int i = 1; i < hostEnd; i++) { - var part = parts.get(i); - if (part instanceof Template.Dynamic d) { - compileExpression(d.toExpression()); - } else { - addLoadConst(part.toString()); - } - hostPartCount++; - } - // Resolve host template to a single string - if (hostPartCount == 1) { - // Already a single value on stack - } else if (hostPartCount > 1) { - writer.writeByte(Opcodes.RESOLVE_TEMPLATE); - writer.writeByte(hostPartCount); - } else { - addLoadConst(""); - } + compileTemplateParts(parts, 1, hostEnd, afterScheme); // Compile path parts if (pathPartIndex > 0) { - int pathPartCount = 0; - for (int i = pathPartIndex; i < parts.size(); i++) { - var part = parts.get(i); - if (part instanceof Template.Dynamic d) { - compileExpression(d.toExpression()); - } else { - addLoadConst(part.toString()); - } - pathPartCount++; - } - if (pathPartCount > 1) { - writer.writeByte(Opcodes.RESOLVE_TEMPLATE); - writer.writeByte(pathPartCount); - } + compileTemplateParts(parts, pathPartIndex, parts.size(), null); } else { addLoadConst(""); } @@ -273,6 +239,104 @@ private static boolean containsUriSpecialChars(List parts) { return false; } + private void compileTemplateParts( + List parts, + int start, + int end, + String leadingLiteral + ) { + boolean hasLeadingLiteral = leadingLiteral != null && !leadingLiteral.isEmpty(); + int segmentCount = end - start + (hasLeadingLiteral ? 1 : 0); + if (segmentCount == 0) { + addLoadConst(""); + return; + } + + if (segmentCount >= MIN_BUILD_TEMPLATE_SEGMENTS + && tryCompileBuildTemplate(parts, start, end, leadingLiteral)) { + return; + } + + if (hasLeadingLiteral) { + addLoadConst(leadingLiteral); + } + for (int i = start; i < end; i++) { + var part = parts.get(i); + if (part instanceof Template.Dynamic dynamic) { + compileExpression(dynamic.toExpression()); + } else { + addLoadConst(part.toString()); + } + } + if (segmentCount > 1) { + writer.writeByte(Opcodes.RESOLVE_TEMPLATE); + writer.writeByte(segmentCount); + } + } + + private boolean tryCompileBuildTemplate( + List parts, + int start, + int end, + String leadingLiteral + ) { + var segments = new ArrayList(end - start + 1); + if (leadingLiteral != null && !leadingLiteral.isEmpty()) { + segments.add(TemplateSegment.literal(leadingLiteral)); + } + + for (int i = start; i < end; i++) { + TemplateSegment segment = createTemplateSegment(parts.get(i)); + if (segment == null) { + return false; + } + segments.add(segment); + } + + if (segments.size() > 255) { + return false; + } + + writer.writeByte(Opcodes.BUILD_TEMPLATE); + writer.writeByte(segments.size()); + for (var segment : segments) { + writer.writeByte(segment.type()); + switch (segment.type()) { + case TemplateSegmentType.LITERAL -> writer.writeShort(writer.getConstantIndex(segment.value())); + case TemplateSegmentType.REGISTER -> writer.writeByte(segment.register()); + case TemplateSegmentType.REGISTER_PROPERTY -> { + writer.writeByte(segment.register()); + writer.writeShort(writer.getConstantIndex(segment.value())); + } + default -> throw new IllegalStateException("Unexpected template segment type: " + segment.type()); + } + } + return true; + } + + private TemplateSegment createTemplateSegment(Template.Part part) { + if (part instanceof Template.Literal literal) { + return TemplateSegment.literal(literal.toString()); + } + if (!(part instanceof Template.Dynamic dynamic)) { + return null; + } + + Expression expression = dynamic.toExpression(); + if (expression instanceof Reference ref) { + return TemplateSegment.register(registerAllocator.getRegister(ref.getName().toString())); + } + if (expression instanceof GetAttr getAttr + && getAttr.getTarget() instanceof Reference ref + && getAttr.getPath().size() == 1 + && getAttr.getPath().get(0) instanceof GetAttr.Part.Key key) { + return TemplateSegment.registerProperty( + registerAllocator.getRegister(ref.getName().toString()), + key.key().toString()); + } + return null; + } + private void compileErrorRule(ErrorRule rule) { compileExpression(rule.getError()); writer.writeByte(Opcodes.RETURN_ERROR); @@ -630,18 +694,7 @@ private void compileLiteral(Literal literal) { // Single dynamic expression, so just evaluate it compileExpression(dynamic.toExpression()); } else { - // Multiple parts - need to concatenate - int expressionCount = 0; - for (var part : parts) { - if (part instanceof Template.Dynamic d) { - compileExpression(d.toExpression()); - } else { - addLoadConst(part.toString()); - } - expressionCount++; - } - writer.writeByte(Opcodes.RESOLVE_TEMPLATE); - writer.writeByte(expressionCount); + compileTemplateParts(parts, 0, parts.size(), null); } } case TupleLiteral t -> { @@ -708,6 +761,20 @@ private void addLoadConst(Object value) { } } + private record TemplateSegment(byte type, byte register, String value) { + static TemplateSegment literal(String value) { + return new TemplateSegment(TemplateSegmentType.LITERAL, (byte) 0, value); + } + + static TemplateSegment register(byte register) { + return new TemplateSegment(TemplateSegmentType.REGISTER, register, null); + } + + static TemplateSegment registerProperty(byte register, String property) { + return new TemplateSegment(TemplateSegmentType.REGISTER_PROPERTY, register, property); + } + } + private Bytecode buildProgram() { var registerDefs = registerAllocator.getRegistry().toArray(new RegisterDefinition[0]); var fns = usedFunctions.toArray(new RulesFunction[0]); diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java index 652b2afd7..ec7151a39 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeDisassembler.java @@ -41,6 +41,7 @@ final class BytecodeDisassembler { // Template operation Map.entry(Opcodes.RESOLVE_TEMPLATE, new InstructionDef("RESOLVE_TEMPLATE", Show.ARG_COUNT)), + Map.entry(Opcodes.BUILD_TEMPLATE, new InstructionDef("BUILD_TEMPLATE", Show.TEMPLATE)), // Function operations Map.entry(Opcodes.FN0, new InstructionDef("FN0", Show.FN)), @@ -106,7 +107,8 @@ private enum Show { SUBSTRING_EQ, SPLIT_GET, SELECT_BOOL, - REG_CONST + REG_CONST, + TEMPLATE } private record InstructionDef(String name, Show show) { @@ -436,6 +438,58 @@ private void appendSymbolicInfo(StringBuilder s, BytecodeWalker walker, Show sho s.append(formatConstant(bytecode.getConstant(constIdx))); } } + case TEMPLATE -> appendTemplate(s, walker); + } + } + + private void appendTemplate(StringBuilder s, BytecodeWalker walker) { + byte[] instructions = bytecode.getBytecode(); + int segmentCount = walker.getOperand(0); + int cursor = walker.getPosition() + 2; + s.append("segments=["); + for (int i = 0; i < segmentCount; i++) { + if (i > 0) { + s.append(", "); + } + + int segmentType = instructions[cursor++] & 0xFF; + switch (segmentType) { + case TemplateSegmentType.LITERAL -> { + int constIdx = ((instructions[cursor] & 0xFF) << 8) | (instructions[cursor + 1] & 0xFF); + cursor += 2; + if (constIdx < bytecode.getConstantPoolCount()) { + s.append(formatConstant(bytecode.getConstant(constIdx))); + } else { + s.append("constant[").append(constIdx).append("]"); + } + } + case TemplateSegmentType.REGISTER -> { + int regIdx = instructions[cursor++] & 0xFF; + appendRegisterName(s, regIdx); + } + case TemplateSegmentType.REGISTER_PROPERTY -> { + int regIdx = instructions[cursor++] & 0xFF; + int propertyIdx = ((instructions[cursor] & 0xFF) << 8) | (instructions[cursor + 1] & 0xFF); + cursor += 2; + appendRegisterName(s, regIdx); + s.append("."); + if (propertyIdx < bytecode.getConstantPoolCount()) { + s.append(bytecode.getConstant(propertyIdx)); + } else { + s.append("constant[").append(propertyIdx).append("]"); + } + } + default -> s.append("unknown[").append(segmentType).append("]"); + } + } + s.append("]"); + } + + private void appendRegisterName(StringBuilder s, int registerIndex) { + if (registerIndex < bytecode.getRegisterDefinitions().length) { + s.append(bytecode.getRegisterDefinitions()[registerIndex].name()); + } else { + s.append("register[").append(registerIndex).append("]"); } } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java index ad0e927ef..b626b8761 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluator.java @@ -380,6 +380,47 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] stack[firstArgPosition] = new String(buf, 0, totalLen); sp = firstArgPosition + 1; } + case Opcodes.BUILD_TEMPLATE -> { + int segmentCount = instructions[pc++] & 0xFF; + int firstArgPosition = sp; + int totalLen = 0; + for (int i = 0; i < segmentCount; i++) { + int segmentType = instructions[pc++] & 0xFF; + String segment = switch (segmentType) { + case TemplateSegmentType.LITERAL -> { + int constIdx = ((instructions[pc] & 0xFF) << 8) + | (instructions[pc + 1] & 0xFF); + pc += 2; + yield (String) constantPool[constIdx]; + } + case TemplateSegmentType.REGISTER -> (String) regs[instructions[pc++] & 0xFF]; + case TemplateSegmentType.REGISTER_PROPERTY -> { + int regIdx = instructions[pc++] & 0xFF; + int propertyIdx = ((instructions[pc] & 0xFF) << 8) + | (instructions[pc + 1] & 0xFF); + pc += 2; + yield (String) EndpointUtils.getProperty( + regs[regIdx], + (String) constantPool[propertyIdx]); + } + default -> throw new RulesEvaluationError( + "Unknown template segment type: " + segmentType, + pc); + }; + totalLen += segment.length(); + stack[sp++] = segment; + } + + char[] buf = getCharBuffer(totalLen); + int pos = 0; + for (int i = firstArgPosition; i < sp; i++) { + String segment = (String) stack[i]; + segment.getChars(0, segment.length(), buf, pos); + pos += segment.length(); + } + stack[firstArgPosition] = new String(buf, 0, totalLen); + sp = firstArgPosition + 1; + } case Opcodes.FN0 -> stack[sp++] = functions[instructions[pc++] & 0xFF].apply0(); case Opcodes.FN1 -> { // Pops 1, pushes 1 - reuse position diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java index 29fe9792e..4e9b6a931 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeWalker.java @@ -67,7 +67,10 @@ public int getInstructionLength() { if (!hasNext()) { return -1; } - int length = getInstructionLength(code.get(pc)); + byte opcode = code.get(pc); + int length = opcode == Opcodes.BUILD_TEMPLATE + ? getBuildTemplateInstructionLength() + : getInstructionLength(opcode); // Validate the full instruction fits within the buffer if (length > 0 && pc + length > code.limit()) { return -1; @@ -93,7 +96,7 @@ public int getOperandCount() { Opcodes.TEST_REGISTER_IS_TRUE, Opcodes.TEST_REGISTER_IS_FALSE, Opcodes.RETURN_ENDPOINT, Opcodes.LOAD_CONST_W, Opcodes.GET_PROPERTY, Opcodes.JNN_OR_POP, Opcodes.GET_NEGATIVE_INDEX, Opcodes.JMP_IF_FALSE, Opcodes.JUMP, Opcodes.SET_REG_RETURN, Opcodes.BUILD_URI, - Opcodes.RESOLVE_TEMPLATE -> + Opcodes.RESOLVE_TEMPLATE, Opcodes.BUILD_TEMPLATE -> 1; case Opcodes.GET_PROPERTY_REG, Opcodes.GET_INDEX_REG, Opcodes.GET_NEGATIVE_INDEX_REG, Opcodes.STRING_EQUALS_REG_CONST -> @@ -161,8 +164,9 @@ public int getOperand(int index) { break; case Opcodes.RESOLVE_TEMPLATE: + case Opcodes.BUILD_TEMPLATE: if (index == 0) { - return code.get(pc + 1) & 0xFF; // arg count + return code.get(pc + 1) & 0xFF; // argument or segment count } break; @@ -225,6 +229,35 @@ public int getOperand(int index) { throw new IllegalArgumentException("Invalid operand index " + index + " for opcode " + opcode); } + private int getBuildTemplateInstructionLength() { + if (pc + 2 > code.limit()) { + return -1; + } + + int segmentCount = code.get(pc + 1) & 0xFF; + int cursor = pc + 2; + for (int i = 0; i < segmentCount; i++) { + if (cursor >= code.limit()) { + return -1; + } + int segmentType = code.get(cursor++) & 0xFF; + int operandLength = switch (segmentType) { + case TemplateSegmentType.LITERAL -> 2; + case TemplateSegmentType.REGISTER -> 1; + case TemplateSegmentType.REGISTER_PROPERTY -> 3; + default -> -1; + }; + if (operandLength < 0) { + return -1; + } + cursor += operandLength; + if (cursor > code.limit()) { + return -1; + } + } + return cursor - pc; + } + public int getJumpTarget() { byte opcode = currentOpcode(); if (opcode == Opcodes.JNN_OR_POP || opcode == Opcodes.JMP_IF_FALSE || opcode == Opcodes.JUMP) { diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java index 8e5b58f67..3ff93ceca 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/Opcodes.java @@ -539,4 +539,20 @@ private Opcodes() {} *

STRUCTN [size:byte] */ public static final byte STRUCTN = 54; + + /** + * Build a string from an inline sequence of literal, register, and register-property segments. + * + *

Stack: [...] => [..., string] + * + *

BUILD_TEMPLATE [segment-count:byte] [segments...] + * + *

Each segment begins with a tag followed by tag-specific operands: + *

    + *
  • {@code LITERAL}: {@code [constant-index:short]}
  • + *
  • {@code REGISTER}: {@code [register-index:byte]}
  • + *
  • {@code REGISTER_PROPERTY}: {@code [register-index:byte] [property-index:short]}
  • + *
+ */ + public static final byte BUILD_TEMPLATE = 55; } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/TemplateSegmentType.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/TemplateSegmentType.java new file mode 100644 index 000000000..ba88e2c94 --- /dev/null +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/TemplateSegmentType.java @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.rulesengine; + +/** + * Segment tags used by the inline {@link Opcodes#BUILD_TEMPLATE} encoding. + */ +final class TemplateSegmentType { + static final byte LITERAL = 0; + static final byte REGISTER = 1; + static final byte REGISTER_PROPERTY = 2; + + private TemplateSegmentType() {} +} diff --git a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java index 2cbcf0cda..3f343cf8a 100644 --- a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java +++ b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeCompilerTest.java @@ -6,10 +6,10 @@ package software.amazon.smithy.java.rulesengine; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -19,12 +19,16 @@ import org.junit.jupiter.api.Test; import software.amazon.smithy.java.context.Context; import software.amazon.smithy.rulesengine.language.Endpoint; +import software.amazon.smithy.rulesengine.language.evaluation.type.Type; import software.amazon.smithy.rulesengine.language.evaluation.value.Value; import software.amazon.smithy.rulesengine.language.syntax.Identifier; import software.amazon.smithy.rulesengine.language.syntax.expressions.Expression; +import software.amazon.smithy.rulesengine.language.syntax.expressions.ExpressionVisitor; import software.amazon.smithy.rulesengine.language.syntax.expressions.Template; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.BooleanEquals; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Coalesce; +import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.FunctionDefinition; +import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.FunctionNode; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.GetAttr; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.IsSet; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.IsValidHostLabel; @@ -222,6 +226,49 @@ void testCompileEndpointRule() { assertOpcodePresent(bytecode, Opcodes.RETURN_ENDPOINT); } + @Test + void testCompileEndpointHostAndPathTemplates() { + Rule rule = EndpointRule.builder() + .endpoint(Endpoint.builder() + .url(Literal.stringLiteral( + Template.fromString("https://{region}.{host}/{bucket}/suffix"))) + .build()); + Parameters params = Parameters.builder() + .addParameter(Parameter.builder() + .name("region") + .type(ParameterType.STRING) + .required(true) + .build()) + .addParameter(Parameter.builder() + .name("host") + .type(ParameterType.STRING) + .required(true) + .build()) + .addParameter(Parameter.builder() + .name("bucket") + .type(ParameterType.STRING) + .required(true) + .build()) + .build(); + EndpointBddTrait bdd = EndpointBddTrait.builder() + .parameters(params) + .conditions(List.of()) + .results(List.of(rule)) + .bdd(new Bdd(100000000, 0, 1, 1, nc -> nc.accept(-1, 1, -1))) + .build(); + + Bytecode bytecode = new BytecodeCompiler( + extensions, + bdd, + functions, + builtinProviders, + Map.of()).compile(); + + assertEquals(2, countOpcode(bytecode, Opcodes.BUILD_TEMPLATE)); + assertOpcodePresent(bytecode, Opcodes.BUILD_URI); + assertOpcodeNotPresent(bytecode, Opcodes.RESOLVE_TEMPLATE); + } + @Test void testCompileEndpointWithHeaders() { Map> headers = new HashMap<>(); @@ -299,9 +346,150 @@ void testCompileStringTemplate() { Bytecode bytecode = compiler.compile(); + assertOpcodePresent(bytecode, Opcodes.BUILD_TEMPLATE); + assertOpcodeNotPresent(bytecode, Opcodes.RESOLVE_TEMPLATE); + } + + @Test + void testCompileTwoSegmentStringTemplateUsesResolveTemplate() { + Template template = Template.fromString("Hello {name}"); + Condition condition = Condition.builder() + .fn(StringEquals.ofExpressions( + Literal.stringLiteral(template), + Literal.stringLiteral(Template.fromString("test")))) + .build(); + + Parameters params = Parameters.builder() + .addParameter(Parameter.builder() + .name("name") + .type(ParameterType.STRING) + .required(true) + .build()) + .build(); + + EndpointBddTrait bdd = createBddWithConditionAndParams(condition, params); + BytecodeCompiler compiler = new BytecodeCompiler(extensions, bdd, functions, builtinProviders, Map.of()); + + Bytecode bytecode = compiler.compile(); + + assertOpcodeNotPresent(bytecode, Opcodes.BUILD_TEMPLATE); assertOpcodePresent(bytecode, Opcodes.RESOLVE_TEMPLATE); } + @Test + void testCompileStringTemplateWithRegisterProperty() { + Condition assignCondition = Condition.builder() + .fn(ParseUrl.ofExpressions(Expression.getReference(Identifier.of("url")))) + .result(Identifier.of("parsedUrl")) + .build(); + Condition templateCondition = Condition.builder() + .fn(StringEquals.ofExpressions( + Literal.stringLiteral(Template.fromString("https://{parsedUrl#authority}/")), + Literal.stringLiteral(Template.fromString("https://example.com/")))) + .build(); + + Parameters params = Parameters.builder() + .addParameter(Parameter.builder() + .name("url") + .type(ParameterType.STRING) + .required(true) + .build()) + .build(); + EndpointBddTrait bdd = EndpointBddTrait.builder() + .parameters(params) + .conditions(List.of(assignCondition, templateCondition)) + .results(List.of(NoMatchRule.INSTANCE)) + .bdd(new Bdd(3, 2, 1, 3, nc -> { + nc.accept(-1, 1, -1); + nc.accept(0, 2, -1); + nc.accept(1, 100000000, -1); + })) + .build(); + + Bytecode bytecode = new BytecodeCompiler( + extensions, + bdd, + functions, + builtinProviders, + Map.of()).compile(); + + assertOpcodePresent(bytecode, Opcodes.BUILD_TEMPLATE); + assertOpcodeNotPresent(bytecode, Opcodes.RESOLVE_TEMPLATE); + } + + @Test + void testCompileStringTemplateFallsBackForNestedPropertyAccess() { + FunctionDefinition nestedRecord = new FunctionDefinition() { + @Override + public String getId() { + return "nestedRecord"; + } + + @Override + public List getArguments() { + return List.of(); + } + + @Override + public Type getReturnType() { + return Type.recordType(Map.of( + Identifier.of("nested"), + Type.recordType(Map.of(Identifier.of("value"), Type.stringType())))); + } + + @Override + public Value evaluate(List arguments) { + return Value.recordValue(Map.of( + Identifier.of("nested"), + Value.recordValue(Map.of(Identifier.of("value"), Value.stringValue("first"))))); + } + + @Override + public LibraryFunction createFunction(FunctionNode functionNode) { + return new LibraryFunction(this, functionNode) { + @Override + public R accept(ExpressionVisitor visitor) { + return visitor.visitLibraryFunction(getFunctionDefinition(), getArguments()); + } + }; + } + }; + LibraryFunction nestedRecordFunction = nestedRecord.createFunction( + FunctionNode.ofExpressions(nestedRecord.getId())); + functions.put(nestedRecord.getId(), new TestFunction(nestedRecord.getId(), 0)); + + Condition assignCondition = Condition.builder() + .fn(nestedRecordFunction) + .result(Identifier.of("metadata")) + .build(); + Condition templateCondition = Condition.builder() + .fn(StringEquals.ofExpressions( + Literal.stringLiteral(Template.fromString("Hello {metadata#nested.value}!")), + Literal.stringLiteral(Template.fromString("Hello first!")))) + .build(); + EndpointBddTrait bdd = EndpointBddTrait.builder() + .parameters(Parameters.builder().build()) + .conditions(List.of(assignCondition, templateCondition)) + .results(List.of(NoMatchRule.INSTANCE)) + .bdd(new Bdd(3, 2, 1, 3, nc -> { + nc.accept(-1, 1, -1); + nc.accept(0, 2, -1); + nc.accept(1, 100000000, -1); + })) + .build(); + + Bytecode bytecode = new BytecodeCompiler( + extensions, + bdd, + functions, + builtinProviders, + Map.of()).compile(); + + assertOpcodeNotPresent(bytecode, Opcodes.BUILD_TEMPLATE); + assertOpcodePresent(bytecode, Opcodes.RESOLVE_TEMPLATE); + assertOpcodePresent(bytecode, Opcodes.GET_PROPERTY); + } + @Test void testCompileTupleLiteral() { // Put tuple literal in endpoint properties to test opcode generation without isSet @@ -677,19 +865,31 @@ void testCompileCoalesce() { } private void assertOpcodePresent(Bytecode bytecode, int expectedOpcode) { - ByteBuffer instructions = bytecode.getInstructions(); - byte[] instructionBytes = new byte[instructions.remaining()]; - instructions.get(instructionBytes); + assertTrue( + findOpcodeWithValue(bytecode, (byte) expectedOpcode).found(), + "Expected opcode " + expectedOpcode + " to be present in bytecode"); + } - boolean found = false; - for (byte b : instructionBytes) { - if ((b & 0xFF) == expectedOpcode) { - found = true; - break; + private void assertOpcodeNotPresent(Bytecode bytecode, int expectedOpcode) { + assertFalse( + findOpcodeWithValue(bytecode, (byte) expectedOpcode).found(), + "Expected opcode " + expectedOpcode + " not to be present in bytecode"); + } + + private int countOpcode(Bytecode bytecode, byte expectedOpcode) { + int count = 0; + for (int i = 0; i < bytecode.getResultCount(); i++) { + BytecodeWalker walker = new BytecodeWalker(bytecode.getInstructions(), bytecode.getResultOffset(i)); + while (walker.hasNext()) { + if (walker.currentOpcode() == expectedOpcode) { + count++; + } + if (walker.isReturnOpcode() || !walker.advance()) { + break; + } } } - - assertTrue(found, "Expected opcode " + expectedOpcode + " to be present in bytecode"); + return count; } private void assertConstantPresent(Bytecode bytecode, Object expectedConstant) { diff --git a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeDisassemblerTest.java b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeDisassemblerTest.java index 97ea865e3..83304de06 100644 --- a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeDisassemblerTest.java +++ b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeDisassemblerTest.java @@ -96,6 +96,36 @@ void disassemblesInstructionWithOperands() { assertThat(result, containsString("parseUrl")); } + @Test + void disassemblesBuildTemplate() { + BytecodeWriter writer = new BytecodeWriter(); + int scheme = writer.getConstantIndex("https://"); + int property = writer.getConstantIndex("suffix"); + + writer.markConditionStart(); + writer.writeByte(Opcodes.BUILD_TEMPLATE); + writer.writeByte(3); + writer.writeByte(TemplateSegmentType.LITERAL); + writer.writeShort(scheme); + writer.writeByte(TemplateSegmentType.REGISTER); + writer.writeByte(0); + writer.writeByte(TemplateSegmentType.REGISTER_PROPERTY); + writer.writeByte(1); + writer.writeShort(property); + writer.writeByte(Opcodes.RETURN_VALUE); + + RegisterDefinition[] registers = { + new RegisterDefinition("region", true, null, null, false), + new RegisterDefinition("metadata", true, null, null, false) + }; + Bytecode bytecode = writer.build(registers, new RulesFunction[0], new int[] {-1, 1, -1}, 1); + + String result = new BytecodeDisassembler(bytecode).disassemble(); + + assertThat(result, containsString("BUILD_TEMPLATE")); + assertThat(result, containsString("segments=[\"https://\", region, metadata.suffix]")); + } + @Test void disassemblesConstantPool() { BytecodeWriter writer = new BytecodeWriter(); diff --git a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluatorTest.java b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluatorTest.java index 756f0ff7f..526978385 100644 --- a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluatorTest.java +++ b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeEvaluatorTest.java @@ -222,6 +222,40 @@ void testResolveTemplate() { evaluator.test(0); } + @Test + void testBuildTemplate() { + int greeting = writer.getConstantIndex("Hello "); + int separator = writer.getConstantIndex(" at "); + int property = writer.getConstantIndex("suffix"); + int expected = writer.getConstantIndex("Hello \u4e16\u754c at example.com"); + + writer.markConditionStart(); + writer.writeByte(Opcodes.BUILD_TEMPLATE); + writer.writeByte(4); + writer.writeByte(TemplateSegmentType.LITERAL); + writer.writeShort(greeting); + writer.writeByte(TemplateSegmentType.REGISTER); + writer.writeByte(0); + writer.writeByte(TemplateSegmentType.LITERAL); + writer.writeShort(separator); + writer.writeByte(TemplateSegmentType.REGISTER_PROPERTY); + writer.writeByte(1); + writer.writeShort(property); + writer.writeByte(Opcodes.LOAD_CONST); + writer.writeByte(expected); + writer.writeByte(Opcodes.STRING_EQUALS); + writer.writeByte(Opcodes.RETURN_VALUE); + + RegisterDefinition[] registers = { + new RegisterDefinition("name", false, "\u4e16\u754c", null, false), + new RegisterDefinition("metadata", false, Map.of("suffix", "example.com"), null, false) + }; + bytecode = buildBytecode(registers); + evaluator = createEvaluator(bytecode); + + assertTrue(evaluator.test(0)); + } + @Test void testJumpNotNullOrPop() { writer.markConditionStart(); diff --git a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeWalkerTest.java b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeWalkerTest.java index a778d6bbf..56cd662ec 100644 --- a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeWalkerTest.java +++ b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/BytecodeWalkerTest.java @@ -111,6 +111,52 @@ void testPropertyAccessInstructions() { assertEquals(5, walker.getOperand(1)); // index } + @Test + void testBuildTemplateInstruction() { + byte[] bytecode = { + Opcodes.BUILD_TEMPLATE, + 3, + TemplateSegmentType.LITERAL, + 0, + 5, + TemplateSegmentType.REGISTER, + 2, + TemplateSegmentType.REGISTER_PROPERTY, + 3, + 0, + 10, + Opcodes.RETURN_VALUE + }; + BytecodeWalker walker = new BytecodeWalker(bytecode); + + assertEquals(11, walker.getInstructionLength()); + assertEquals(1, walker.getOperandCount()); + assertEquals(3, walker.getOperand(0)); + assertTrue(walker.advance()); + assertEquals(11, walker.getPosition()); + assertEquals(Opcodes.RETURN_VALUE, walker.currentOpcode()); + } + + @Test + void rejectsMalformedBuildTemplateInstruction() { + BytecodeWalker unknownSegment = new BytecodeWalker(new byte[] { + Opcodes.BUILD_TEMPLATE, + 1, + 99 + }); + BytecodeWalker truncatedSegment = new BytecodeWalker(new byte[] { + Opcodes.BUILD_TEMPLATE, + 1, + TemplateSegmentType.REGISTER_PROPERTY, + 0 + }); + + assertEquals(-1, unknownSegment.getInstructionLength()); + assertFalse(unknownSegment.advance()); + assertEquals(-1, truncatedSegment.getInstructionLength()); + assertFalse(truncatedSegment.advance()); + } + @Test void testEmptyBytecode() { BytecodeWalker walker = new BytecodeWalker(new byte[0]); diff --git a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/RulesEngineBuilderTest.java b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/RulesEngineBuilderTest.java index 37a982a48..860700247 100644 --- a/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/RulesEngineBuilderTest.java +++ b/rulesengine/src/test/java/software/amazon/smithy/java/rulesengine/RulesEngineBuilderTest.java @@ -148,6 +148,26 @@ void testLoadInvalidVersion() throws IOException { assertTrue(ex.getMessage().contains("Unsupported bytecode version")); } + @Test + void testLoadPreviousBytecodeVersion() { + BytecodeWriter writer = new BytecodeWriter(); + writer.markConditionStart(); + writer.writeByte(Opcodes.LOAD_CONST); + writer.writeByte(writer.getConstantIndex(Boolean.TRUE)); + writer.writeByte(Opcodes.RETURN_VALUE); + byte[] data = writer.build( + new RegisterDefinition[0], + new RulesFunction[0], + new int[] {-1, 1, -1}, + 1).getBytecode().clone(); + data[4] = 0; + data[5] = 1; + + Bytecode loaded = builder.load(data); + + assertEquals(1, loaded.getVersion()); + } + @Test void testLoadTooShortBytecode() { byte[] bytecode = new byte[43]; // One byte too short