diff --git a/aws/client/aws-client-rulesengine/src/jmh/java/software/amazon/smithy/java/aws/client/rulesengine/S3EndpointBenchmark.java b/aws/client/aws-client-rulesengine/src/jmh/java/software/amazon/smithy/java/aws/client/rulesengine/S3EndpointBenchmark.java index b26f3a4d2..50c862b8e 100644 --- a/aws/client/aws-client-rulesengine/src/jmh/java/software/amazon/smithy/java/aws/client/rulesengine/S3EndpointBenchmark.java +++ b/aws/client/aws-client-rulesengine/src/jmh/java/software/amazon/smithy/java/aws/client/rulesengine/S3EndpointBenchmark.java @@ -339,4 +339,49 @@ public Object vanillaAccessPointArn(ParamState params) { public Object s3OutpostsVanilla(ParamState params) { return params.resolver.resolveEndpoint(params.s3OutpostsVanillaParams); } + + /** + * Cycles through different bucket names on each invocation to defeat the URI cache hot-slot. + * This measures the cost of URI construction on cache miss. + */ + @State(Scope.Thread) + public static class VaryingBucketState { + private static final int BUCKET_COUNT = 64; + private EndpointResolverParams[] paramVariants; + private int index; + + @Setup + public void setup(ParamState params) { + boolean canned = "canned".equals(params.paramMode); + var client = SharedResolver.CLIENT; + var getObject = SharedResolver.GET_OBJECT; + paramVariants = new EndpointResolverParams[BUCKET_COUNT]; + for (int i = 0; i < BUCKET_COUNT; i++) { + String bucket = "bucket-" + i; + paramVariants[i] = ParamState.buildParams(client, + getObject, + canned, + Map.of("Bucket", Document.of(bucket), "Key", Document.of("key")), + "us-west-2", + Map.of("Accelerate", + false, + "Bucket", + bucket, + "ForcePathStyle", + false, + "Region", + "us-west-2", + "UseDualStack", + false, + "UseFIPS", + false)); + } + } + } + + @Benchmark + public Object vanillaVirtualAddressingVaryingBucket(ParamState params, VaryingBucketState varying) { + var p = varying.paramVariants[varying.index++ & (VaryingBucketState.BUCKET_COUNT - 1)]; + return params.resolver.resolveEndpoint(p); + } } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ArrayPropertyGetter.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ArrayPropertyGetter.java new file mode 100644 index 000000000..51d201d35 --- /dev/null +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ArrayPropertyGetter.java @@ -0,0 +1,22 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.java.rulesengine; + +/** + * Lightweight PropertyGetter backed by parallel key/value arrays. + * More efficient than Map for small fixed-key lookups (linear scan beats hashing for ~4 entries). + */ +record ArrayPropertyGetter(String[] keys, Object[] values) implements PropertyGetter { + @Override + public Object getProperty(String name) { + for (int i = 0; i < keys.length; i++) { + if (name.equals(keys[i])) { + return values[i]; + } + } + return null; + } +} 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 7fd899e61..1b45239b3 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 @@ -178,6 +178,17 @@ public final class Bytecode { private final int[] hardRequiredIndices; private final Map inputRegisterMap; + // Inline condition types for fast BDD evaluation. + static final byte COND_ISSET = 1; + static final byte COND_IS_TRUE = 2; + static final byte COND_IS_FALSE = 3; + static final byte COND_NOT_SET = 4; + static final byte COND_STRING_EQ_REG_CONST = 5; + + // Condition classification arrays for inline BDD evaluation + final byte[] conditionTypes; + final int[] conditionOperands; + private Bdd bdd; Bytecode( @@ -230,6 +241,67 @@ public final class Bytecode { this.hardRequiredIndices = findRequiredIndicesWithoutDefaultsOrBuiltins(registerDefinitions); this.inputRegisterMap = createInputRegisterMap(registerDefinitions); this.version = version; + + // Classify conditions for inline BDD evaluation + this.conditionTypes = new byte[conditionOffsets.length]; + this.conditionOperands = new int[conditionOffsets.length]; + classifyConditions(); + } + + private void classifyConditions() { + int len = bytecode.length; + for (int i = 0; i < conditionOffsets.length; i++) { + int offset = conditionOffsets[i]; + + // 2-byte opcode patterns: (need offset+2 in bounds) + if (offset + 2 < len) { + int firstOpcode = bytecode[offset] & 0xFF; + int reg = bytecode[offset + 1] & 0xFF; + int next = bytecode[offset + 2] & 0xFF; + // Only inline conditions that end with RETURN_VALUE (no binding). + // Conditions with SET_REG_RETURN have a side effect (register write) that + // the inline path cannot replicate — they must go through full bytecode eval. + boolean isReturn = next == (Opcodes.RETURN_VALUE & 0xFF); + + if (isReturn) { + switch (firstOpcode) { + case Opcodes.TEST_REGISTER_ISSET & 0xFF -> { + conditionTypes[i] = COND_ISSET; + conditionOperands[i] = reg; + continue; + } + case Opcodes.TEST_REGISTER_IS_TRUE & 0xFF -> { + conditionTypes[i] = COND_IS_TRUE; + conditionOperands[i] = reg; + continue; + } + case Opcodes.TEST_REGISTER_IS_FALSE & 0xFF -> { + conditionTypes[i] = COND_IS_FALSE; + conditionOperands[i] = reg; + continue; + } + case Opcodes.TEST_REGISTER_NOT_SET & 0xFF -> { + conditionTypes[i] = COND_NOT_SET; + conditionOperands[i] = reg; + continue; + } + default -> { + } + } + } + } + + // 4-byte opcode pattern: STRING_EQUALS_REG_CONST [reg:1] [const:2] + if (offset + 4 < len && (bytecode[offset] & 0xFF) == (Opcodes.STRING_EQUALS_REG_CONST & 0xFF)) { + int reg = bytecode[offset + 1] & 0xFF; + int constIdx = ((bytecode[offset + 2] & 0xFF) << 8) | (bytecode[offset + 3] & 0xFF); + int next = bytecode[offset + 4] & 0xFF; + if (next == (Opcodes.RETURN_VALUE & 0xFF)) { + conditionTypes[i] = COND_STRING_EQ_REG_CONST; + conditionOperands[i] = (constIdx << 8) | reg; + } + } + } } /** 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 458e61143..774065856 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 @@ -113,12 +113,14 @@ Bytecode compile() { private void compileCondition(Condition condition) { compileExpression(condition.getFunction()); - condition.getResult().ifPresent(result -> { - byte register = registerAllocator.getOrAllocateRegister(result.toString()); - writer.writeByte(Opcodes.SET_REGISTER); + var result = condition.getResult(); + if (result.isPresent()) { + byte register = registerAllocator.getOrAllocateRegister(result.get().toString()); + writer.writeByte(Opcodes.SET_REG_RETURN); writer.writeByte(register); - }); - writer.writeByte(Opcodes.RETURN_VALUE); + } else { + writer.writeByte(Opcodes.RETURN_VALUE); + } } private void compileEndpointRule(EndpointRule rule) { @@ -150,7 +152,7 @@ private void compileEndpointRule(EndpointRule rule) { compileMapCreation(e.getProperties().size()); } - compileExpression(e.getUrl()); + compileEndpointUrl(e.getUrl()); // Add the return endpoint instruction writer.writeByte(Opcodes.RETURN_ENDPOINT); @@ -164,6 +166,95 @@ private void compileEndpointRule(EndpointRule rule) { writer.writeByte(packed); } + /** + * Compile the URL expression for an endpoint, attempting to decompose it into scheme/host/path + * so that BUILD_URI can construct a SmithyUri directly without string-to-URI parsing. + * + *

Falls back to compileExpression (producing a String) if the URL can't be decomposed. + */ + private void compileEndpointUrl(Expression urlExpression) { + // Only optimize StringLiteral templates with multiple parts where the first part + // is a literal starting with a scheme (e.g., "https://...") + if (urlExpression instanceof StringLiteral sl) { + var template = sl.value(); + var parts = template.getParts(); + if (parts.size() > 1 && parts.getFirst() instanceof Template.Literal firstLit) { + String firstStr = firstLit.toString(); + int schemeEnd = firstStr.indexOf("://"); + if (schemeEnd > 0) { + String scheme = firstStr.substring(0, schemeEnd); + String afterScheme = firstStr.substring(schemeEnd + 3); + + // Find where path starts: look for a part that is "/" or starts with "/" + // The host is everything between "://" and the first "/" separator + // In most S3 templates, there's no explicit "/" — the path is empty + // In some, there's a "/" part followed by bucket + url.path + int pathPartIndex = -1; + for (int i = 0; i < parts.size(); i++) { + if (parts.get(i) instanceof Template.Literal lit && lit.toString().startsWith("/") + && i > 0) { + pathPartIndex = i; + break; + } + } + + // 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(""); + } + + // 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); + } + } else { + addLoadConst(""); + } + + // BUILD_URI pops host and path, pushes SmithyUri + writer.writeByte(Opcodes.BUILD_URI); + writer.writeShort(writer.getConstantIndex(scheme)); + return; + } + } + } + // Fallback: compile as regular string expression + compileExpression(urlExpression); + } + private void compileErrorRule(ErrorRule rule) { compileExpression(rule.getError()); writer.writeByte(Opcodes.RETURN_ERROR); @@ -288,6 +379,20 @@ public Void visitStringEquals(Expression left, Expression right) { } } } + if (left instanceof Reference ref && right instanceof StringLiteral sl + && sl.value().getParts().size() == 1) { + writer.writeByte(Opcodes.STRING_EQUALS_REG_CONST); + writer.writeByte(registerAllocator.getRegister(ref.getName().toString())); + writer.writeShort(writer.getConstantIndex(sl.value().getParts().get(0).toString())); + return null; + } + if (right instanceof Reference ref && left instanceof StringLiteral sl + && sl.value().getParts().size() == 1) { + writer.writeByte(Opcodes.STRING_EQUALS_REG_CONST); + writer.writeByte(registerAllocator.getRegister(ref.getName().toString())); + writer.writeShort(writer.getConstantIndex(sl.value().getParts().get(0).toString())); + return null; + } compileExpression(left); compileExpression(right); writer.writeByte(Opcodes.STRING_EQUALS); @@ -519,7 +624,14 @@ private void compileLiteral(Literal literal) { compileLiteral(e.getValue()); // value then key to make popping ordered addLoadConst(e.getKey().toString()); } - compileMapCreation(r.members().size()); + int size = r.members().size(); + if (size <= 8) { + // Small records: PropertyGetter with linear scan beats Map hashing + writer.writeByte(Opcodes.STRUCTN); + writer.writeByte(size); + } else { + compileMapCreation(size); + } } case BooleanLiteral b -> addLoadConst(b.value().getValue()); case IntegerLiteral i -> addLoadConst(i.toNode().expectNumberNode().getValue()); 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 47b47cb68..a4fbb6035 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 @@ -84,7 +84,11 @@ final class BytecodeDisassembler { Map.entry(Opcodes.JMP_IF_FALSE, new InstructionDef("JMP_IF_FALSE", Show.JUMP_OFFSET)), Map.entry(Opcodes.JUMP, new InstructionDef("JUMP", Show.JUMP_OFFSET)), Map.entry(Opcodes.SUBSTRING_EQ, new InstructionDef("SUBSTRING_EQ", Show.SUBSTRING_EQ)), - Map.entry(Opcodes.SPLIT_GET, new InstructionDef("SPLIT_GET", Show.SPLIT_GET))); + Map.entry(Opcodes.SPLIT_GET, new InstructionDef("SPLIT_GET", Show.SPLIT_GET)), + Map.entry(Opcodes.STRING_EQUALS_REG_CONST, new InstructionDef("STRING_EQUALS_REG_CONST", Show.REG_CONST)), + Map.entry(Opcodes.SET_REG_RETURN, new InstructionDef("SET_REG_RETURN", Show.REGISTER)), + Map.entry(Opcodes.BUILD_URI, new InstructionDef("BUILD_URI", Show.CONST)), + Map.entry(Opcodes.STRUCTN, new InstructionDef("STRUCTN", Show.NUMBER))); private enum Show { CONST, @@ -99,7 +103,8 @@ private enum Show { PROPERTY, ARG_COUNT, SUBSTRING_EQ, - SPLIT_GET + SPLIT_GET, + REG_CONST } private record InstructionDef(String name, Show show) { @@ -257,11 +262,9 @@ private void writeInstruction(StringBuilder s, BytecodeWalker walker, String ind int value = walker.getOperand(i); // Format based on operand width if (opcode == Opcodes.LOAD_CONST_W || opcode == Opcodes.GET_PROPERTY - || - opcode == Opcodes.JNN_OR_POP + || opcode == Opcodes.JNN_OR_POP || (opcode == Opcodes.GET_PROPERTY_REG && i == 1) - || - (opcode == Opcodes.RESOLVE_TEMPLATE && i == 1)) { + || (opcode == Opcodes.STRING_EQUALS_REG_CONST && i == 1)) { s.append(String.format("%5d", value)); } else { s.append(String.format("%3d", value)); @@ -380,6 +383,17 @@ private void appendSymbolicInfo(StringBuilder s, BytecodeWalker walker, Show sho } s.append(")[").append(index).append("]"); } + case REG_CONST -> { + int regIndex = walker.getOperand(0); + int constIdx = walker.getOperand(1); + if (regIndex >= 0 && regIndex < bytecode.getRegisterDefinitions().length) { + s.append(bytecode.getRegisterDefinitions()[regIndex].name()); + } + s.append(" == "); + if (constIdx >= 0 && constIdx < bytecode.getConstantPoolCount()) { + s.append(formatConstant(bytecode.getConstant(constIdx))); + } + } } } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEndpointResolver.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEndpointResolver.java index 50dae8682..7ae5d6824 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEndpointResolver.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/BytecodeEndpointResolver.java @@ -13,7 +13,6 @@ import software.amazon.smithy.java.endpoints.EndpointResolver; import software.amazon.smithy.java.endpoints.EndpointResolverParams; import software.amazon.smithy.java.logging.InternalLogger; -import software.amazon.smithy.rulesengine.logic.bdd.Bdd; /** * Endpoint resolver that uses a compiled endpoint rules program from a BDD. @@ -23,7 +22,6 @@ public final class BytecodeEndpointResolver implements EndpointResolver { private static final InternalLogger LOGGER = InternalLogger.getLogger(BytecodeEndpointResolver.class); private final Bytecode bytecode; - private final Bdd bdd; private final RulesExtension[] extensions; private final RegisterFiller registerFiller; private final ContextProvider ctxProvider = new ContextProvider.OrchestratingProvider(); @@ -36,7 +34,6 @@ public BytecodeEndpointResolver( ) { this.bytecode = bytecode; this.extensions = extensions.toArray(new RulesExtension[0]); - this.bdd = bytecode.getBdd(); // Create and reuse this register filler across thread local evaluators. this.registerFiller = RegisterFiller.of(bytecode, builtinProviders); @@ -64,10 +61,6 @@ public Endpoint resolveEndpoint(EndpointResolverParams params) { LOGGER.debug("Resolving endpoint of {} using VM", operation); - var resultIndex = bdd.evaluate(evaluator); - if (resultIndex < 0) { - return null; - } - return evaluator.resolveResult(resultIndex); + return evaluator.evaluateBdd(); } } 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 a0be0bf15..133441064 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 @@ -14,11 +14,12 @@ import software.amazon.smithy.java.context.Context; import software.amazon.smithy.java.endpoints.Endpoint; import software.amazon.smithy.java.endpoints.EndpointContext; +import software.amazon.smithy.java.io.uri.SmithyUri; import software.amazon.smithy.java.io.uri.URLEncoding; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.IsValidHostLabel; import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Split; -import software.amazon.smithy.rulesengine.language.syntax.expressions.functions.Substring; import software.amazon.smithy.rulesengine.logic.ConditionEvaluator; +import software.amazon.smithy.rulesengine.logic.bdd.Bdd; /** * Evaluates bytecode for a single specific condition or result per evaluation. @@ -35,10 +36,14 @@ final class BytecodeEvaluator implements ConditionEvaluator { private Object[] stack = new Object[64]; // 64 is more than enough for virtual any ruleset private int stackPosition = 0; private int pc; - private final StringBuilder stringBuilder = new StringBuilder(64); + private char[] charBuffer = new char[128]; private final UriFactory uriFactory = new UriFactory(); private final RegisterFiller registerFiller; private Context context; + // Hot-slot cache for BUILD_URI + private String cachedUriHost; + private String cachedUriPath; + private SmithyUri cachedUri; BytecodeEvaluator(Bytecode bytecode, RulesExtension[] extensions, RegisterFiller registerFiller) { this.bytecode = bytecode; @@ -81,6 +86,47 @@ public boolean test(int conditionIndex) { return result != null && result != Boolean.FALSE; } + /** + * Evaluates the BDD with inline condition optimization. + * Trivial conditions (register checks, string equality) are evaluated directly + * without entering the full bytecode dispatch loop. + */ + Endpoint evaluateBdd() { + int ref = bytecode.getBddRootRef(); + int[] nodes = bytecode.getBddNodes(); + byte[] condTypes = bytecode.conditionTypes; + int[] condOps = bytecode.conditionOperands; + Object[] regs = this.registers; + Object[] cpool = bytecode.getConstantPool(); + + while (Bdd.isNodeReference(ref)) { + int idx = ref > 0 ? ref - 1 : -ref - 1; + int base = idx * 3; + int condIdx = nodes[base]; + + boolean result = switch (condTypes[condIdx]) { + case Bytecode.COND_ISSET -> regs[condOps[condIdx]] != null; + case Bytecode.COND_IS_TRUE -> regs[condOps[condIdx]] == Boolean.TRUE; + case Bytecode.COND_IS_FALSE -> regs[condOps[condIdx]] == Boolean.FALSE; + case Bytecode.COND_NOT_SET -> regs[condOps[condIdx]] == null; + case Bytecode.COND_STRING_EQ_REG_CONST -> { + int packed = condOps[condIdx]; + String s = (String) regs[packed & 0xFF]; + String expected = (String) cpool[packed >>> 8]; + yield s != null && s.equals(expected); + } + default -> test(condIdx); + }; + + ref = (result ^ (ref < 0)) ? nodes[base + 1] : nodes[base + 2]; + } + + if (Bdd.isTerminal(ref)) { + return null; + } + return resolveResult(ref - Bdd.RESULT_OFFSET); + } + public Endpoint resolveResult(int resultIndex) { if (resultIndex <= -1) { return null; @@ -94,12 +140,6 @@ private void push(Object value) { stack[stackPosition++] = value; } - private void resizeStack() { - Object[] newStack = new Object[stack.length + (stack.length >> 1)]; - System.arraycopy(stack, 0, newStack, 0, stack.length); - stack = newStack; - } - private Object[] getTempArray(int requiredSize) { return tempArraySize >= requiredSize ? tempArray : resizeTempArray(requiredSize); } @@ -112,6 +152,14 @@ private Object[] resizeTempArray(int requiredSize) { return tempArray; } + private char[] getCharBuffer(int requiredSize) { + if (charBuffer.length >= requiredSize) { + return charBuffer; + } + charBuffer = new char[Math.max(requiredSize, charBuffer.length * 2)]; + return charBuffer; + } + private Object run(int start) { pc = start; @@ -134,9 +182,7 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] while (pc < instructions.length) { int opcode = instructions[pc++] & 0xFF; switch (opcode) { - case Opcodes.LOAD_CONST -> { - push(constantPool[instructions[pc++] & 0xFF]); - } + case Opcodes.LOAD_CONST -> push(constantPool[instructions[pc++] & 0xFF]); case Opcodes.LOAD_CONST_W -> { int constIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); push(constantPool[constIdx]); @@ -206,7 +252,6 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] stackPosition = idx + 1; } case Opcodes.MAP3 -> { - // Pops 6, pushes 1 int idx = stackPosition - 6; stack[idx] = Map.of( (String) stack[idx + 2], // key @@ -218,7 +263,6 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] stackPosition = idx + 1; } case Opcodes.MAP4 -> { - // Pops 8, pushes 1 int idx = stackPosition - 8; stack[idx] = Map.of( (String) stack[idx + 1], // key @@ -237,24 +281,36 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] for (var i = 0; i < size; i++) { map.put((String) stack[--stackPosition], stack[--stackPosition]); } - push(map); // dynamic size + push(map); + } + case Opcodes.STRUCTN -> { + var size = instructions[pc++] & 0xFF; + var keys = new String[size]; + var values = new Object[size]; + for (var i = 0; i < size; i++) { + keys[i] = (String) stack[--stackPosition]; + values[i] = stack[--stackPosition]; + } + push(new ArrayPropertyGetter(keys, values)); } case Opcodes.RESOLVE_TEMPLATE -> { int argCount = instructions[pc++] & 0xFF; - stringBuilder.setLength(0); - // Calculate where the first argument is on the stack int firstArgPosition = stackPosition - argCount; + int totalLen = 0; for (int i = 0; i < argCount; i++) { - stringBuilder.append(stack[firstArgPosition + i]); + totalLen += ((String) stack[firstArgPosition + i]).length(); } - // Result goes where first arg was - stack[firstArgPosition] = stringBuilder.toString(); + char[] buf = getCharBuffer(totalLen); + int pos = 0; + for (int i = 0; i < argCount; i++) { + String s = (String) stack[firstArgPosition + i]; + s.getChars(0, s.length(), buf, pos); + pos += s.length(); + } + stack[firstArgPosition] = new String(buf, 0, totalLen); stackPosition = firstArgPosition + 1; } - case Opcodes.FN0 -> { - var fn = functions[instructions[pc++] & 0xFF]; - push(fn.apply0()); - } + case Opcodes.FN0 -> push(functions[instructions[pc++] & 0xFF].apply0()); case Opcodes.FN1 -> { // Pops 1, pushes 1 - reuse position var fn = functions[instructions[pc++] & 0xFF]; @@ -269,7 +325,6 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] stackPosition = idx + 1; } case Opcodes.FN3 -> { - // Pops 3, pushes 1 var fn = functions[instructions[pc++] & 0xFF]; int idx = stackPosition - 3; stack[idx] = fn.apply(stack[idx], stack[idx + 1], stack[idx + 2]); @@ -281,7 +336,7 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] for (int i = fn.getArgumentCount() - 1; i >= 0; i--) { temp[i] = stack[--stackPosition]; } - push(fn.apply(temp)); // dynamic size + push(fn.apply(temp)); } case Opcodes.GET_PROPERTY -> { int propertyIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); @@ -348,10 +403,9 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] var startPos = instructions[pc++] & 0xFF; var endPos = instructions[pc++] & 0xFF; var reverse = (instructions[pc++] & 0xFF) != 0; - stack[idx] = Substring.getSubstring(string, startPos, endPos, reverse); + stack[idx] = EndpointUtils.getSubstring(string, startPos, endPos, reverse); } case Opcodes.IS_VALID_HOST_LABEL -> { - // Pops 2, pushes 1 int idx = stackPosition - 2; var hostLabel = (String) stack[idx]; var allowDots = (Boolean) stack[idx + 1]; @@ -375,10 +429,14 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] var packed = instructions[pc++]; boolean hasHeaders = (packed & 1) != 0; boolean hasProperties = (packed & 2) != 0; - var urlString = (String) stack[--stackPosition]; + var urlValue = stack[--stackPosition]; var properties = (Map) (hasProperties ? stack[--stackPosition] : Map.of()); var headers = (Map>) (hasHeaders ? stack[--stackPosition] : Map.of()); - var builder = Endpoint.builder().uri(uriFactory.createUri(urlString)); + // URL may be a SmithyUri (from BUILD_URI) or String (legacy/fallback) + SmithyUri uri = urlValue instanceof SmithyUri su + ? su + : uriFactory.createUri((String) urlValue); + var builder = Endpoint.builder().uri(uri); if (!headers.isEmpty()) { builder.putProperty(EndpointContext.HEADERS, headers); } @@ -402,7 +460,6 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] } } case Opcodes.SPLIT -> { - // Pops 3, pushes 1 int idx = stackPosition - 3; var string = (String) stack[idx]; var delimiter = (String) stack[idx + 1]; @@ -418,8 +475,7 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] case Opcodes.GET_NEGATIVE_INDEX_REG -> { int regIndex = instructions[pc++] & 0xFF; int index = instructions[pc++] & 0xFF; - var target = registers[regIndex]; - push(EndpointUtils.getNegativeIndex(target, index)); + push(EndpointUtils.getNegativeIndex(registers[regIndex], index)); } case Opcodes.JMP_IF_FALSE -> { Object condition = stack[--stackPosition]; @@ -459,10 +515,44 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] var sgDelimiter = (String) constantPool[sgDelimIdx]; push(EndpointUtils.splitGet(sgValue, sgDelimiter, sgIndex)); } + case Opcodes.STRING_EQUALS_REG_CONST -> { + int srcRegIndex = instructions[pc++] & 0xFF; + int srcConstIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + var srcValue = (String) registers[srcRegIndex]; + var srcExpected = (String) constantPool[srcConstIdx]; + push(srcValue != null && srcValue.equals(srcExpected) ? Boolean.TRUE : Boolean.FALSE); + } + case Opcodes.SET_REG_RETURN -> { + int srIndex = instructions[pc++] & 0xFF; + Object srValue = stack[--stackPosition]; + registers[srIndex] = srValue; + return srValue; + } + case Opcodes.BUILD_URI -> { + int schemeIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + int idx = stackPosition - 2; + var host = (String) stack[idx]; + var path = (String) stack[idx + 1]; + // Hot-slot cache: scheme is always a constant, so only check host+path + if (host != null && host.equals(cachedUriHost) && path.equals(cachedUriPath)) { + stack[idx] = cachedUri; + } else { + var scheme = (String) constantPool[schemeIdx]; + var uri = SmithyUri.of(scheme, host, -1, path, null); + cachedUriHost = host; + cachedUriPath = path; + cachedUri = uri; + stack[idx] = uri; + } + stackPosition = idx + 1; + } default -> throw new RulesEvaluationError("Unknown rules engine instruction: " + opcode, pc); } } throw new IllegalArgumentException("Expected to return a value during evaluation"); } + } 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 e6ee03547..24b52704d 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 @@ -88,13 +88,15 @@ public int getOperandCount() { Opcodes.RETURN_ERROR, Opcodes.RETURN_VALUE, Opcodes.SPLIT -> 0; case Opcodes.LOAD_CONST, Opcodes.SET_REGISTER, Opcodes.LOAD_REGISTER, Opcodes.TEST_REGISTER_ISSET, - Opcodes.TEST_REGISTER_NOT_SET, Opcodes.LISTN, Opcodes.MAPN, Opcodes.FN0, Opcodes.FN1, Opcodes.FN2, - Opcodes.FN3, Opcodes.FN, Opcodes.GET_INDEX, 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.TEST_REGISTER_NOT_SET, Opcodes.LISTN, Opcodes.MAPN, Opcodes.STRUCTN, Opcodes.FN0, + Opcodes.FN1, Opcodes.FN2, Opcodes.FN3, Opcodes.FN, Opcodes.GET_INDEX, + 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 -> 1; - case Opcodes.GET_PROPERTY_REG, Opcodes.GET_INDEX_REG, Opcodes.RESOLVE_TEMPLATE, - Opcodes.GET_NEGATIVE_INDEX_REG -> + case Opcodes.GET_PROPERTY_REG, Opcodes.GET_INDEX_REG, Opcodes.GET_NEGATIVE_INDEX_REG, + Opcodes.STRING_EQUALS_REG_CONST -> 2; case Opcodes.SUBSTRING, Opcodes.SPLIT_GET -> 3; case Opcodes.SUBSTRING_EQ -> 5; @@ -114,6 +116,7 @@ public int getOperand(int index) { case Opcodes.TEST_REGISTER_NOT_SET: case Opcodes.LISTN: case Opcodes.MAPN: + case Opcodes.STRUCTN: case Opcodes.FN0: case Opcodes.FN1: case Opcodes.FN2: @@ -123,6 +126,7 @@ public int getOperand(int index) { case Opcodes.TEST_REGISTER_IS_TRUE: case Opcodes.TEST_REGISTER_IS_FALSE: case Opcodes.RETURN_ENDPOINT: + case Opcodes.SET_REG_RETURN: if (index == 0) { return code.get(pc + 1) & 0xFF; } @@ -132,6 +136,7 @@ public int getOperand(int index) { case Opcodes.LOAD_CONST_W: case Opcodes.GET_PROPERTY: case Opcodes.JNN_OR_POP: + case Opcodes.BUILD_URI: if (index == 0) { return ((code.get(pc + 1) & 0xFF) << 8) | (code.get(pc + 2) & 0xFF); } @@ -139,10 +144,11 @@ public int getOperand(int index) { // Mixed operand instructions case Opcodes.GET_PROPERTY_REG: + case Opcodes.STRING_EQUALS_REG_CONST: if (index == 0) { return code.get(pc + 1) & 0xFF; // register } else if (index == 1) { - return ((code.get(pc + 2) & 0xFF) << 8) | (code.get(pc + 3) & 0xFF); // property index + return ((code.get(pc + 2) & 0xFF) << 8) | (code.get(pc + 3) & 0xFF); // const/property index } break; @@ -157,8 +163,6 @@ public int getOperand(int index) { case Opcodes.RESOLVE_TEMPLATE: if (index == 0) { return code.get(pc + 1) & 0xFF; // arg count - } else if (index == 1) { - return ((code.get(pc + 2) & 0xFF) << 8) | (code.get(pc + 3) & 0xFF); // template index } break; @@ -222,7 +226,9 @@ public int getJumpTarget() { public boolean isReturnOpcode() { byte op = currentOpcode(); - return op == Opcodes.RETURN_VALUE || op == Opcodes.RETURN_ENDPOINT || op == Opcodes.RETURN_ERROR; + return op == Opcodes.RETURN_VALUE || op == Opcodes.RETURN_ENDPOINT + || op == Opcodes.RETURN_ERROR + || op == Opcodes.SET_REG_RETURN; } public static int getInstructionLength(byte opcode) { @@ -233,14 +239,16 @@ public static int getInstructionLength(byte opcode) { Opcodes.RETURN_ERROR, Opcodes.RETURN_VALUE, Opcodes.SPLIT -> 1; case Opcodes.LOAD_CONST, Opcodes.SET_REGISTER, Opcodes.LOAD_REGISTER, Opcodes.TEST_REGISTER_ISSET, - Opcodes.TEST_REGISTER_NOT_SET, Opcodes.LISTN, Opcodes.MAPN, Opcodes.FN0, Opcodes.FN1, Opcodes.FN2, - Opcodes.FN3, Opcodes.FN, Opcodes.GET_INDEX, Opcodes.TEST_REGISTER_IS_TRUE, - Opcodes.TEST_REGISTER_IS_FALSE, Opcodes.RETURN_ENDPOINT, Opcodes.GET_NEGATIVE_INDEX -> + Opcodes.TEST_REGISTER_NOT_SET, Opcodes.LISTN, Opcodes.MAPN, Opcodes.STRUCTN, Opcodes.FN0, + Opcodes.FN1, Opcodes.FN2, Opcodes.FN3, Opcodes.FN, Opcodes.GET_INDEX, + Opcodes.TEST_REGISTER_IS_TRUE, Opcodes.TEST_REGISTER_IS_FALSE, Opcodes.RETURN_ENDPOINT, + Opcodes.GET_NEGATIVE_INDEX, Opcodes.SET_REG_RETURN, Opcodes.RESOLVE_TEMPLATE -> 2; case Opcodes.LOAD_CONST_W, Opcodes.GET_PROPERTY, Opcodes.JNN_OR_POP, Opcodes.GET_INDEX_REG, - Opcodes.GET_NEGATIVE_INDEX_REG, Opcodes.JMP_IF_FALSE, Opcodes.JUMP -> + Opcodes.GET_NEGATIVE_INDEX_REG, Opcodes.JMP_IF_FALSE, Opcodes.JUMP, Opcodes.BUILD_URI -> 3; - case Opcodes.RESOLVE_TEMPLATE, Opcodes.GET_PROPERTY_REG, Opcodes.SUBSTRING -> 4; + case Opcodes.GET_PROPERTY_REG, Opcodes.SUBSTRING, Opcodes.STRING_EQUALS_REG_CONST -> + 4; case Opcodes.SPLIT_GET -> 5; case Opcodes.SUBSTRING_EQ -> 7; default -> -1; diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ContextProvider.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ContextProvider.java index aa92a7ec3..f36055303 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ContextProvider.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ContextProvider.java @@ -46,13 +46,22 @@ final class RegisterSink { void put(String name, Object value) { Integer i = registerMap.get(name); if (i != null) { - values[i] = value; - if (i < 64) { - filled |= 1L << i; - } + putIndex(i, value); } } + void putIndex(int i, Object value) { + values[i] = value; + if (i < 64) { + filled |= 1L << i; + } + } + + int resolveIndex(String name) { + Integer i = registerMap.get(name); + return i != null ? i : -1; + } + void putAll(Map map) { for (var e : map.entrySet()) { put(e.getKey(), e.getValue()); @@ -125,7 +134,17 @@ private ContextProvider createProvider(ApiOperation operation) { } // Find the smithy.rules#staticContextParams on the operation. - record StaticParamsProvider(Map params) implements ContextProvider { + static final class StaticParamsProvider implements ContextProvider { + private final Map params; + // Pre-resolved: parallel arrays of index to value for direct sink writes. + // resolvedIndices acts as the publication guard — volatile write after both arrays are ready. + private volatile int[] resolvedIndices; + private Object[] resolvedValues; + + StaticParamsProvider(Map params) { + this.params = params; + } + @Override public void addContext(ApiOperation operation, SerializableStruct input, Map params) { params.putAll(this.params); @@ -133,7 +152,37 @@ public void addContext(ApiOperation operation, SerializableStruct input, M @Override public void addContext(ApiOperation operation, SerializableStruct input, RegisterSink sink) { - sink.putAll(this.params); + int[] indices = resolvedIndices; + if (indices == null) { + indices = resolveIndices(sink); + } + Object[] values = resolvedValues; + for (int i = 0; i < indices.length; i++) { + sink.putIndex(indices[i], values[i]); + } + } + + private int[] resolveIndices(RegisterSink sink) { + int count = 0; + int[] indices = new int[params.size()]; + Object[] values = new Object[params.size()]; + for (var entry : params.entrySet()) { + int idx = sink.resolveIndex(entry.getKey()); + if (idx >= 0) { + indices[count] = idx; + values[count] = entry.getValue(); + count++; + } + } + // Trim to actual size + if (count != indices.length) { + indices = java.util.Arrays.copyOf(indices, count); + values = java.util.Arrays.copyOf(values, count); + } + // Write resolvedValues before resolvedIndices to ensure publication + resolvedValues = values; + resolvedIndices = indices; + return indices; } static void compute(List providers, Schema operation) { @@ -152,7 +201,16 @@ static void compute(List providers, Schema operation) { } // Find smithy.rules#contextParam trait on operation input members. - record ContextParamProvider(Schema member, String name) implements ContextProvider { + final class ContextParamProvider implements ContextProvider { + private final Schema member; + private final String name; + private volatile int cachedIndex = -2; // -2 = not resolved yet + + ContextParamProvider(Schema member, String name) { + this.member = member; + this.name = name; + } + @Override public void addContext(ApiOperation operation, SerializableStruct input, Map params) { var value = input.getMemberValue(member); @@ -165,7 +223,14 @@ public void addContext(ApiOperation operation, SerializableStruct input, M public void addContext(ApiOperation operation, SerializableStruct input, RegisterSink sink) { var value = input.getMemberValue(member); if (value != null) { - sink.put(name, value); + int idx = cachedIndex; + if (idx == -2) { + idx = sink.resolveIndex(name); + cachedIndex = idx; + } + if (idx >= 0) { + sink.putIndex(idx, value); + } } } diff --git a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/EndpointUtils.java b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/EndpointUtils.java index 229492850..6ba9aaa77 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/EndpointUtils.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/EndpointUtils.java @@ -122,6 +122,32 @@ static Object getNegativeIndex(Object target, int negIndex) { return null; } + // TODO: Remove when Substring.getSubstring in smithy is updated to only check the substring range for ASCII. + // Fast substring that only validates ASCII within the actual substring range, not the entire string. + static String getSubstring(String value, int startIndex, int stopIndex, boolean reverse) { + if (value == null) { + return null; + } + int len = value.length(); + if (startIndex >= stopIndex || len < stopIndex) { + return null; + } + int actualStart, actualEnd; + if (reverse) { + actualStart = len - stopIndex; + actualEnd = len - startIndex; + } else { + actualStart = startIndex; + actualEnd = stopIndex; + } + for (int i = actualStart; i < actualEnd; i++) { + if (value.charAt(i) > 127) { + return null; + } + } + return value.substring(actualStart, actualEnd); + } + // Check if substring equals expected, returning false for null/short strings static boolean substringEquals(String value, int start, int end, boolean reverse, String expected) { if (value == null || expected == null) { 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 2103e02f9..db9ae6448 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 @@ -178,7 +178,7 @@ private Opcodes() {} * *

Stack: [..., arg1, arg2, ..., argN] => [..., string] * - *

RESOLVE_TEMPLATE [arg-count:byte] [template-index:short] + *

RESOLVE_TEMPLATE [arg-count:byte] */ public static final byte RESOLVE_TEMPLATE = 18; @@ -487,4 +487,46 @@ private Opcodes() {} *

Index is signed: positive = from start, negative = from end (-1 = last) */ public static final byte SPLIT_GET = 49; + + /** + * Compare a register's string value against a constant string. + * Pushes true if the register value equals the constant, false otherwise. + * Handles null register values (returns false). + * + *

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

STRING_EQUALS_REG_CONST [register:byte] [const-index:short] + */ + public static final byte STRING_EQUALS_REG_CONST = 48; + + /** + * Store the value at the top of the stack into a register and return it. + * Combines SET_REGISTER + RETURN_VALUE into a single instruction. + * + *

Stack: [..., value] => (returns value) + * + *

SET_REG_RETURN [register:byte] + */ + public static final byte SET_REG_RETURN = 50; + + /** + * Pop host and path strings from the stack, build a SmithyUri using a scheme from the constant pool. + * Pushes the resulting SmithyUri onto the stack. The scheme is always a compile-time constant. + * + *

Stack: [..., host, path] => [..., SmithyUri] + * + *

BUILD_URI [scheme-const:short] + */ + public static final byte BUILD_URI = 51; + + /** + * Pop N key-value pairs from the stack and push a PropertyGetter backed by parallel arrays. + * Same stack layout as MAPN, but produces a lightweight PropertyGetter instead of a Map. + * More efficient for small fixed-key maps (e.g., auth scheme properties). + * + *

Stack: [..., value1, key1, ..., valueN, keyN] => [..., PropertyGetter] + * + *

STRUCTN [size:byte] + */ + public static final byte STRUCTN = 52; } 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 08193b564..4537f0be7 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 @@ -127,7 +127,7 @@ void testCompileConditionWithBinding() { } assertTrue(foundHasInput); - assertOpcodePresent(bytecode, Opcodes.SET_REGISTER); + assertOpcodePresent(bytecode, Opcodes.SET_REG_RETURN); } @Test @@ -346,7 +346,7 @@ void testCompileRecordLiteral() { Bytecode bytecode = compiler.compile(); - assertOpcodePresent(bytecode, Opcodes.MAP2); + assertOpcodePresent(bytecode, Opcodes.STRUCTN); } @Test @@ -619,7 +619,7 @@ void testCompileLargeMap() { Bytecode bytecode = compiler.compile(); - assertOpcodePresent(bytecode, Opcodes.MAPN); + assertOpcodePresent(bytecode, Opcodes.STRUCTN); } @Test