From 9b77add9f2c8dc552cb5d2a66b59f08655b6d024 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Mon, 6 Apr 2026 19:43:33 -0700 Subject: [PATCH 1/5] Add varying bucket benchmarks --- .../rulesengine/S3EndpointBenchmark.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) 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); + } } From 01e6a6e300e2feebcdad831459131ba59f551001 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Mon, 6 Apr 2026 20:02:34 -0700 Subject: [PATCH 2/5] Rules engine optimizations --- .../java/rulesengine/ArrayPropertyGetter.java | 22 +++ .../smithy/java/rulesengine/Bytecode.java | 72 ++++++++ .../java/rulesengine/BytecodeCompiler.java | 126 +++++++++++++- .../rulesengine/BytecodeDisassembler.java | 25 ++- .../rulesengine/BytecodeEndpointResolver.java | 9 +- .../java/rulesengine/BytecodeEvaluator.java | 154 ++++++++++++++---- .../java/rulesengine/BytecodeWalker.java | 38 +++-- .../java/rulesengine/ContextProvider.java | 81 ++++++++- .../java/rulesengine/EndpointUtils.java | 26 +++ .../smithy/java/rulesengine/Opcodes.java | 44 ++++- .../rulesengine/BytecodeCompilerTest.java | 6 +- 11 files changed, 525 insertions(+), 78 deletions(-) create mode 100644 rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ArrayPropertyGetter.java 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 ab00d2989..0392df1c5 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); @@ -532,7 +637,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 22b532293..1f353bd3e 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 @@ -85,7 +85,12 @@ final class BytecodeDisassembler { 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.SELECT_BOOL_REG, new InstructionDef("SELECT_BOOL_REG", Show.SELECT_BOOL))); + Map.entry(Opcodes.SELECT_BOOL_REG, new InstructionDef("SELECT_BOOL_REG", Show.SELECT_BOOL))), + 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, @@ -101,7 +106,9 @@ private enum Show { ARG_COUNT, SUBSTRING_EQ, SPLIT_GET, - SELECT_BOOL + SELECT_BOOL, + SPLIT_GET, + REG_CONST } private record InstructionDef(String name, Show show) { @@ -263,8 +270,7 @@ private void writeInstruction(StringBuilder s, BytecodeWalker walker, String ind opcode == Opcodes.JNN_OR_POP || (opcode == Opcodes.GET_PROPERTY_REG && i == 1) || (opcode == Opcodes.SELECT_BOOL_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)); @@ -399,6 +405,17 @@ private void appendSymbolicInfo(StringBuilder s, BytecodeWalker walker, Show sho s.append(formatConstant(bytecode.getConstant(falseIdx))); } } + 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 0d02033e6..e2ffc0676 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]; @@ -469,10 +525,44 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] ? constantPool[selTrue] : constantPool[selFalse]); } + 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 1ca47e015..29fe9792e 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, Opcodes.SELECT_BOOL_REG -> 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; @@ -232,7 +236,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) { @@ -243,14 +249,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.SELECT_BOOL_REG -> 6; case Opcodes.SUBSTRING_EQ -> 7; 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 fbee561bd..8e5b58f67 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; @@ -497,4 +497,46 @@ private Opcodes() {} *

SELECT_BOOL_REG [register:byte] [true-const:short] [false-const:short] */ public static final byte SELECT_BOOL_REG = 50; + + /** + * 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 = 51; + + /** + * 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 = 52; + + /** + * 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 = 53; + + /** + * 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 = 54; } 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 a0aa79f8a..2cbcf0cda 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 From ba8074bb109d77e1f7ae563232c3075360d0b4e5 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Tue, 7 Apr 2026 15:28:21 -0700 Subject: [PATCH 3/5] Do not optimize URLs with query strings and switch to interleaved arrays in ArrayPropertyGetter --- .../java/rulesengine/ArrayPropertyGetter.java | 11 ++++++----- .../java/rulesengine/BytecodeCompiler.java | 18 ++++++++++++++++++ .../java/rulesengine/BytecodeEvaluator.java | 13 ++++++------- 3 files changed, 30 insertions(+), 12 deletions(-) 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 index 51d201d35..2bf593ea8 100644 --- a/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ArrayPropertyGetter.java +++ b/rulesengine/src/main/java/software/amazon/smithy/java/rulesengine/ArrayPropertyGetter.java @@ -6,15 +6,16 @@ package software.amazon.smithy.java.rulesengine; /** - * Lightweight PropertyGetter backed by parallel key/value arrays. + * Lightweight PropertyGetter backed by an interleaved value/key array. + * Layout: [value0, key0, value1, key1, ...]. Keys are at odd indices. * More efficient than Map for small fixed-key lookups (linear scan beats hashing for ~4 entries). */ -record ArrayPropertyGetter(String[] keys, Object[] values) implements PropertyGetter { +record ArrayPropertyGetter(Object[] data) implements PropertyGetter { @Override public Object getProperty(String name) { - for (int i = 0; i < keys.length; i++) { - if (name.equals(keys[i])) { - return values[i]; + for (int i = 1; i < data.length; i += 2) { + if (name.equals(data[i])) { + return data[i - 1]; } } return null; 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 0392df1c5..801476fcd 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 @@ -180,6 +180,12 @@ private void compileEndpointUrl(Expression urlExpression) { var parts = template.getParts(); if (parts.size() > 1 && parts.getFirst() instanceof Template.Literal firstLit) { String firstStr = firstLit.toString(); + // Bail out if any literal part contains characters that indicate + // query strings or userInfo — these need full URI.create() parsing. + if (containsUriSpecialChars(parts)) { + compileExpression(urlExpression); + return; + } int schemeEnd = firstStr.indexOf("://"); if (schemeEnd > 0) { String scheme = firstStr.substring(0, schemeEnd); @@ -255,6 +261,18 @@ private void compileEndpointUrl(Expression urlExpression) { compileExpression(urlExpression); } + private static boolean containsUriSpecialChars(List parts) { + for (var part : parts) { + if (part instanceof Template.Literal lit) { + String s = lit.toString(); + if (s.indexOf('?') >= 0 || s.indexOf('@') >= 0) { + return true; + } + } + } + return false; + } + private void compileErrorRule(ErrorRule rule) { compileExpression(rule.getError()); writer.writeByte(Opcodes.RETURN_ERROR); 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 e2ffc0676..c01ff41ff 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 @@ -285,13 +285,12 @@ private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] } 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)); + int totalSlots = size * 2; + int base = stackPosition - totalSlots; + var data = new Object[totalSlots]; + System.arraycopy(stack, base, data, 0, totalSlots); + stack[base] = new ArrayPropertyGetter(data); + stackPosition = base + 1; } case Opcodes.RESOLVE_TEMPLATE -> { int argCount = instructions[pc++] & 0xFF; From 88e2ef4155e91efde181fe927bc0abc334e9f380 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Tue, 7 Apr 2026 16:54:19 -0700 Subject: [PATCH 4/5] Rebasing changes --- .../amazon/smithy/java/rulesengine/BytecodeDisassembler.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 1f353bd3e..8a594058d 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 @@ -85,8 +85,7 @@ final class BytecodeDisassembler { 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.SELECT_BOOL_REG, new InstructionDef("SELECT_BOOL_REG", Show.SELECT_BOOL))), - Map.entry(Opcodes.SPLIT_GET, new InstructionDef("SPLIT_GET", Show.SPLIT_GET)), + Map.entry(Opcodes.SELECT_BOOL_REG, new InstructionDef("SELECT_BOOL_REG", Show.SELECT_BOOL)), 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)), @@ -107,7 +106,6 @@ private enum Show { SUBSTRING_EQ, SPLIT_GET, SELECT_BOOL, - SPLIT_GET, REG_CONST } From a4dbf0dc4ad1cda9b656e988ba8c50c569075c37 Mon Sep 17 00:00:00 2001 From: Adwait Kumar Singh Date: Tue, 7 Apr 2026 17:03:52 -0700 Subject: [PATCH 5/5] Localize interpreter state --- .../java/rulesengine/BytecodeEvaluator.java | 763 +++++++++--------- 1 file changed, 385 insertions(+), 378 deletions(-) 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 c01ff41ff..ff0d9298a 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 @@ -136,10 +136,6 @@ public Endpoint resolveResult(int resultIndex) { } } - private void push(Object value) { - stack[stackPosition++] = value; - } - private Object[] getTempArray(int requiredSize) { return tempArraySize >= requiredSize ? tempArray : resizeTempArray(requiredSize); } @@ -178,390 +174,401 @@ private Object run(int start) { @SuppressWarnings("unchecked") private Object runLoop(byte[] instructions, RulesFunction[] functions, Object[] constantPool) { + // Localize hot interpreter state to avoid field access on every instruction. + // Written back in finally for error reporting. + int pc = this.pc; + int sp = this.stackPosition; + Object[] stack = this.stack; + Object[] regs = this.registers; - while (pc < instructions.length) { - int opcode = instructions[pc++] & 0xFF; - switch (opcode) { - 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]); - pc += 2; - } - case Opcodes.SET_REGISTER -> { - int index = instructions[pc++] & 0xFF; - registers[index] = stack[stackPosition - 1]; - } - case Opcodes.LOAD_REGISTER -> { - int index = instructions[pc++] & 0xFF; - push(registers[index]); - } - case Opcodes.NOT -> { - // In-place operation - int idx = stackPosition - 1; - stack[idx] = stack[idx] == Boolean.FALSE ? Boolean.TRUE : Boolean.FALSE; - } - case Opcodes.ISSET -> { - // In-place operation - int idx = stackPosition - 1; - stack[idx] = stack[idx] != null ? Boolean.TRUE : Boolean.FALSE; - } - case Opcodes.TEST_REGISTER_ISSET -> { - push(registers[instructions[pc++] & 0xFF] != null ? Boolean.TRUE : Boolean.FALSE); - } - case Opcodes.TEST_REGISTER_NOT_SET -> { - push(registers[instructions[pc++] & 0xFF] == null ? Boolean.TRUE : Boolean.FALSE); - } - // List operations - case Opcodes.LIST0 -> push(Collections.emptyList()); - case Opcodes.LIST1 -> { - // Pops 1, pushes 1: reuse position - int idx = stackPosition - 1; - stack[idx] = List.of(stack[idx]); - } - case Opcodes.LIST2 -> { - // Pops 2, pushes 1 - int idx = stackPosition - 2; - stack[idx] = List.of(stack[idx], stack[idx + 1]); - stackPosition = idx + 1; - } - case Opcodes.LISTN -> { - var size = instructions[pc++] & 0xFF; - var values = new Object[size]; - for (var i = size - 1; i >= 0; i--) { - values[i] = stack[--stackPosition]; + try { + while (pc < instructions.length) { + int opcode = instructions[pc++] & 0xFF; + switch (opcode) { + case Opcodes.LOAD_CONST -> stack[sp++] = constantPool[instructions[pc++] & 0xFF]; + case Opcodes.LOAD_CONST_W -> { + int constIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + stack[sp++] = constantPool[constIdx]; + pc += 2; } - push(Arrays.asList(values)); // dynamic size - } - // Map operations - case Opcodes.MAP0 -> push(Map.of()); - case Opcodes.MAP1 -> { - // Pops 2, pushes 1 - int idx = stackPosition - 2; - stack[idx] = Map.of((String) stack[idx + 1], stack[idx]); - stackPosition = idx + 1; - } - case Opcodes.MAP2 -> { - // Pops 4, pushes 1 - int idx = stackPosition - 4; - stack[idx] = Map.of( - (String) stack[idx + 1], // key - stack[idx], // value - (String) stack[idx + 3], // key - stack[idx + 2]); // value - stackPosition = idx + 1; - } - case Opcodes.MAP3 -> { - int idx = stackPosition - 6; - stack[idx] = Map.of( - (String) stack[idx + 2], // key - stack[idx + 1], // value - (String) stack[idx + 4], // key - stack[idx + 3], // value - (String) stack[idx + 5], // key - stack[idx]); - stackPosition = idx + 1; - } - case Opcodes.MAP4 -> { - int idx = stackPosition - 8; - stack[idx] = Map.of( - (String) stack[idx + 1], // key - stack[idx], // value - (String) stack[idx + 3], // key - stack[idx + 2], // value - (String) stack[idx + 5], // key - stack[idx + 4], // value - (String) stack[idx + 7], // key - stack[idx + 6]); // value - stackPosition = idx + 1; - } - case Opcodes.MAPN -> { - var size = instructions[pc++] & 0xFF; - Map map = new HashMap<>(size + 1, 1.0f); - for (var i = 0; i < size; i++) { - map.put((String) stack[--stackPosition], stack[--stackPosition]); + case Opcodes.SET_REGISTER -> { + int index = instructions[pc++] & 0xFF; + regs[index] = stack[sp - 1]; } - push(map); - } - case Opcodes.STRUCTN -> { - var size = instructions[pc++] & 0xFF; - int totalSlots = size * 2; - int base = stackPosition - totalSlots; - var data = new Object[totalSlots]; - System.arraycopy(stack, base, data, 0, totalSlots); - stack[base] = new ArrayPropertyGetter(data); - stackPosition = base + 1; - } - case Opcodes.RESOLVE_TEMPLATE -> { - int argCount = instructions[pc++] & 0xFF; - int firstArgPosition = stackPosition - argCount; - int totalLen = 0; - for (int i = 0; i < argCount; i++) { - totalLen += ((String) stack[firstArgPosition + i]).length(); - } - 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 -> push(functions[instructions[pc++] & 0xFF].apply0()); - case Opcodes.FN1 -> { - // Pops 1, pushes 1 - reuse position - var fn = functions[instructions[pc++] & 0xFF]; - int idx = stackPosition - 1; - stack[idx] = fn.apply1(stack[idx]); - } - case Opcodes.FN2 -> { - // Pops 2, pushes 1 - var fn = functions[instructions[pc++] & 0xFF]; - int idx = stackPosition - 2; - stack[idx] = fn.apply2(stack[idx], stack[idx + 1]); - stackPosition = idx + 1; - } - case Opcodes.FN3 -> { - var fn = functions[instructions[pc++] & 0xFF]; - int idx = stackPosition - 3; - stack[idx] = fn.apply(stack[idx], stack[idx + 1], stack[idx + 2]); - stackPosition = idx + 1; - } - case Opcodes.FN -> { - var fn = functions[instructions[pc++] & 0xFF]; - var temp = getTempArray(fn.getArgumentCount()); - for (int i = fn.getArgumentCount() - 1; i >= 0; i--) { - temp[i] = stack[--stackPosition]; + case Opcodes.LOAD_REGISTER -> { + int index = instructions[pc++] & 0xFF; + stack[sp++] = regs[index]; } - push(fn.apply(temp)); - } - case Opcodes.GET_PROPERTY -> { - int propertyIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - var propertyName = (String) constantPool[propertyIdx]; - int idx = stackPosition - 1; - stack[idx] = EndpointUtils.getProperty(stack[idx], propertyName); - pc += 2; - } - case Opcodes.GET_INDEX -> { - int index = instructions[pc++] & 0xFF; - int idx = stackPosition - 1; - stack[idx] = EndpointUtils.getIndex(stack[idx], index); - } - case Opcodes.GET_PROPERTY_REG -> { - int regIndex = instructions[pc++] & 0xFF; - int propertyIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - var propertyName = (String) constantPool[propertyIdx]; - var target = registers[regIndex]; - push(EndpointUtils.getProperty(target, propertyName)); - pc += 2; - } - case Opcodes.GET_INDEX_REG -> { - int regIndex = instructions[pc++] & 0xFF; - int index = instructions[pc++] & 0xFF; - var target = registers[regIndex]; - push(EndpointUtils.getIndex(target, index)); - } - case Opcodes.IS_TRUE -> { - // In-place operation - int idx = stackPosition - 1; - stack[idx] = stack[idx] == Boolean.TRUE ? Boolean.TRUE : Boolean.FALSE; - } - case Opcodes.TEST_REGISTER_IS_TRUE -> { - push(registers[instructions[pc++] & 0xFF] == Boolean.TRUE ? Boolean.TRUE : Boolean.FALSE); - } - case Opcodes.TEST_REGISTER_IS_FALSE -> { - push(registers[instructions[pc++] & 0xFF] == Boolean.FALSE ? Boolean.TRUE : Boolean.FALSE); - } - case Opcodes.EQUALS -> { - // Pops 2, pushes 1 - int idx = stackPosition - 2; - stack[idx] = Objects.equals(stack[idx], stack[idx + 1]) ? Boolean.TRUE : Boolean.FALSE; - stackPosition = idx + 1; - } - case Opcodes.STRING_EQUALS -> { - // Pops 2, pushes 1 - int idx = stackPosition - 2; - var a = (String) stack[idx]; - var b = (String) stack[idx + 1]; - stack[idx] = a != null && a.equals(b) ? Boolean.TRUE : Boolean.FALSE; - stackPosition = idx + 1; - } - case Opcodes.BOOLEAN_EQUALS -> { - // Pops 2, pushes 1 - int idx = stackPosition - 2; - var a = (Boolean) stack[idx]; - var b = (Boolean) stack[idx + 1]; - stack[idx] = a != null && a.equals(b) ? Boolean.TRUE : Boolean.FALSE; - stackPosition = idx + 1; - } - case Opcodes.SUBSTRING -> { - int idx = stackPosition - 1; - var string = (String) stack[idx]; - var startPos = instructions[pc++] & 0xFF; - var endPos = instructions[pc++] & 0xFF; - var reverse = (instructions[pc++] & 0xFF) != 0; - stack[idx] = EndpointUtils.getSubstring(string, startPos, endPos, reverse); - } - case Opcodes.IS_VALID_HOST_LABEL -> { - int idx = stackPosition - 2; - var hostLabel = (String) stack[idx]; - var allowDots = (Boolean) stack[idx + 1]; - stack[idx] = IsValidHostLabel.isValidHostLabel(hostLabel, Boolean.TRUE.equals(allowDots)) - ? Boolean.TRUE - : Boolean.FALSE; - stackPosition = idx + 1; - } - case Opcodes.PARSE_URL -> { - int idx = stackPosition - 1; - var urlString = (String) stack[idx]; - stack[idx] = urlString == null ? null : uriFactory.createUri(urlString); - } - case Opcodes.URI_ENCODE -> { - int idx = stackPosition - 1; - var string = (String) stack[idx]; - stack[idx] = URLEncoding.encodeUnreserved(string, false); - } - case Opcodes.RETURN_ERROR -> throw new RulesEvaluationError((String) stack[--stackPosition], pc); - case Opcodes.RETURN_ENDPOINT -> { - var packed = instructions[pc++]; - boolean hasHeaders = (packed & 1) != 0; - boolean hasProperties = (packed & 2) != 0; - var urlValue = stack[--stackPosition]; - var properties = (Map) (hasProperties ? stack[--stackPosition] : Map.of()); - var headers = (Map>) (hasHeaders ? stack[--stackPosition] : Map.of()); - // 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); - } - for (var extension : extensions) { - extension.extractEndpointProperties(builder, context, properties, headers); - } - return builder.build(); - } - case Opcodes.RETURN_VALUE -> { - return stack[--stackPosition]; - } - case Opcodes.JNN_OR_POP -> { - Object value = stack[stackPosition - 1]; - // Read as unsigned 16-bit value (0-65535) - int offset = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - if (value != null) { - pc += offset; // Jump forward, keeping value on stack - } else { - stackPosition--; // Pop the null value + case Opcodes.NOT -> { + // In-place operation + int idx = sp - 1; + stack[idx] = stack[idx] == Boolean.FALSE ? Boolean.TRUE : Boolean.FALSE; } - } - case Opcodes.SPLIT -> { - int idx = stackPosition - 3; - var string = (String) stack[idx]; - var delimiter = (String) stack[idx + 1]; - var limit = ((Number) stack[idx + 2]).intValue(); - stack[idx] = Split.split(string, delimiter, limit); - stackPosition = idx + 1; - } - case Opcodes.GET_NEGATIVE_INDEX -> { - int index = instructions[pc++] & 0xFF; - int idx = stackPosition - 1; - stack[idx] = EndpointUtils.getNegativeIndex(stack[idx], index); - } - case Opcodes.GET_NEGATIVE_INDEX_REG -> { - int regIndex = instructions[pc++] & 0xFF; - int index = instructions[pc++] & 0xFF; - push(EndpointUtils.getNegativeIndex(registers[regIndex], index)); - } - case Opcodes.JMP_IF_FALSE -> { - Object condition = stack[--stackPosition]; - // Read as unsigned 16-bit value (0-65535) - int offset = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - if (condition != Boolean.TRUE) { + case Opcodes.ISSET -> { + // In-place operation + int idx = sp - 1; + stack[idx] = stack[idx] != null ? Boolean.TRUE : Boolean.FALSE; + } + case Opcodes.TEST_REGISTER_ISSET -> { + stack[sp++] = regs[instructions[pc++] & 0xFF] != null ? Boolean.TRUE : Boolean.FALSE; + } + case Opcodes.TEST_REGISTER_NOT_SET -> { + stack[sp++] = regs[instructions[pc++] & 0xFF] == null ? Boolean.TRUE : Boolean.FALSE; + } + // List operations + case Opcodes.LIST0 -> stack[sp++] = Collections.emptyList(); + case Opcodes.LIST1 -> { + // Pops 1, pushes 1: reuse position + int idx = sp - 1; + stack[idx] = List.of(stack[idx]); + } + case Opcodes.LIST2 -> { + // Pops 2, pushes 1 + int idx = sp - 2; + stack[idx] = List.of(stack[idx], stack[idx + 1]); + sp = idx + 1; + } + case Opcodes.LISTN -> { + var size = instructions[pc++] & 0xFF; + var values = new Object[size]; + for (var i = size - 1; i >= 0; i--) { + values[i] = stack[--sp]; + } + stack[sp++] = Arrays.asList(values); // dynamic size + } + // Map operations + case Opcodes.MAP0 -> stack[sp++] = Map.of(); + case Opcodes.MAP1 -> { + // Pops 2, pushes 1 + int idx = sp - 2; + stack[idx] = Map.of((String) stack[idx + 1], stack[idx]); + sp = idx + 1; + } + case Opcodes.MAP2 -> { + // Pops 4, pushes 1 + int idx = sp - 4; + stack[idx] = Map.of( + (String) stack[idx + 1], // key + stack[idx], // value + (String) stack[idx + 3], // key + stack[idx + 2]); // value + sp = idx + 1; + } + case Opcodes.MAP3 -> { + int idx = sp - 6; + stack[idx] = Map.of( + (String) stack[idx + 2], // key + stack[idx + 1], // value + (String) stack[idx + 4], // key + stack[idx + 3], // value + (String) stack[idx + 5], // key + stack[idx]); + sp = idx + 1; + } + case Opcodes.MAP4 -> { + int idx = sp - 8; + stack[idx] = Map.of( + (String) stack[idx + 1], // key + stack[idx], // value + (String) stack[idx + 3], // key + stack[idx + 2], // value + (String) stack[idx + 5], // key + stack[idx + 4], // value + (String) stack[idx + 7], // key + stack[idx + 6]); // value + sp = idx + 1; + } + case Opcodes.MAPN -> { + var size = instructions[pc++] & 0xFF; + Map map = new HashMap<>(size + 1, 1.0f); + for (var i = 0; i < size; i++) { + map.put((String) stack[--sp], stack[--sp]); + } + stack[sp++] = map; + } + case Opcodes.STRUCTN -> { + var size = instructions[pc++] & 0xFF; + int totalSlots = size * 2; + int base = sp - totalSlots; + var data = new Object[totalSlots]; + System.arraycopy(stack, base, data, 0, totalSlots); + stack[base] = new ArrayPropertyGetter(data); + sp = base + 1; + } + case Opcodes.RESOLVE_TEMPLATE -> { + int argCount = instructions[pc++] & 0xFF; + int firstArgPosition = sp - argCount; + int totalLen = 0; + for (int i = 0; i < argCount; i++) { + totalLen += ((String) stack[firstArgPosition + i]).length(); + } + 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); + sp = firstArgPosition + 1; + } + case Opcodes.FN0 -> stack[sp++] = functions[instructions[pc++] & 0xFF].apply0(); + case Opcodes.FN1 -> { + // Pops 1, pushes 1 - reuse position + var fn = functions[instructions[pc++] & 0xFF]; + int idx = sp - 1; + stack[idx] = fn.apply1(stack[idx]); + } + case Opcodes.FN2 -> { + // Pops 2, pushes 1 + var fn = functions[instructions[pc++] & 0xFF]; + int idx = sp - 2; + stack[idx] = fn.apply2(stack[idx], stack[idx + 1]); + sp = idx + 1; + } + case Opcodes.FN3 -> { + var fn = functions[instructions[pc++] & 0xFF]; + int idx = sp - 3; + stack[idx] = fn.apply(stack[idx], stack[idx + 1], stack[idx + 2]); + sp = idx + 1; + } + case Opcodes.FN -> { + var fn = functions[instructions[pc++] & 0xFF]; + var temp = getTempArray(fn.getArgumentCount()); + for (int i = fn.getArgumentCount() - 1; i >= 0; i--) { + temp[i] = stack[--sp]; + } + stack[sp++] = fn.apply(temp); + } + case Opcodes.GET_PROPERTY -> { + int propertyIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + var propertyName = (String) constantPool[propertyIdx]; + int idx = sp - 1; + stack[idx] = EndpointUtils.getProperty(stack[idx], propertyName); + pc += 2; + } + case Opcodes.GET_INDEX -> { + int index = instructions[pc++] & 0xFF; + int idx = sp - 1; + stack[idx] = EndpointUtils.getIndex(stack[idx], index); + } + case Opcodes.GET_PROPERTY_REG -> { + int regIndex = instructions[pc++] & 0xFF; + int propertyIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + var propertyName = (String) constantPool[propertyIdx]; + var target = regs[regIndex]; + stack[sp++] = EndpointUtils.getProperty(target, propertyName); + pc += 2; + } + case Opcodes.GET_INDEX_REG -> { + int regIndex = instructions[pc++] & 0xFF; + int index = instructions[pc++] & 0xFF; + var target = regs[regIndex]; + stack[sp++] = EndpointUtils.getIndex(target, index); + } + case Opcodes.IS_TRUE -> { + // In-place operation + int idx = sp - 1; + stack[idx] = stack[idx] == Boolean.TRUE ? Boolean.TRUE : Boolean.FALSE; + } + case Opcodes.TEST_REGISTER_IS_TRUE -> { + stack[sp++] = regs[instructions[pc++] & 0xFF] == Boolean.TRUE ? Boolean.TRUE : Boolean.FALSE; + } + case Opcodes.TEST_REGISTER_IS_FALSE -> { + stack[sp++] = regs[instructions[pc++] & 0xFF] == Boolean.FALSE ? Boolean.TRUE : Boolean.FALSE; + } + case Opcodes.EQUALS -> { + // Pops 2, pushes 1 + int idx = sp - 2; + stack[idx] = Objects.equals(stack[idx], stack[idx + 1]) ? Boolean.TRUE : Boolean.FALSE; + sp = idx + 1; + } + case Opcodes.STRING_EQUALS -> { + // Pops 2, pushes 1 + int idx = sp - 2; + var a = (String) stack[idx]; + var b = (String) stack[idx + 1]; + stack[idx] = a != null && a.equals(b) ? Boolean.TRUE : Boolean.FALSE; + sp = idx + 1; + } + case Opcodes.BOOLEAN_EQUALS -> { + // Pops 2, pushes 1 + int idx = sp - 2; + var a = (Boolean) stack[idx]; + var b = (Boolean) stack[idx + 1]; + stack[idx] = a != null && a.equals(b) ? Boolean.TRUE : Boolean.FALSE; + sp = idx + 1; + } + case Opcodes.SUBSTRING -> { + int idx = sp - 1; + var string = (String) stack[idx]; + var startPos = instructions[pc++] & 0xFF; + var endPos = instructions[pc++] & 0xFF; + var reverse = (instructions[pc++] & 0xFF) != 0; + stack[idx] = EndpointUtils.getSubstring(string, startPos, endPos, reverse); + } + case Opcodes.IS_VALID_HOST_LABEL -> { + int idx = sp - 2; + var hostLabel = (String) stack[idx]; + var allowDots = (Boolean) stack[idx + 1]; + stack[idx] = IsValidHostLabel.isValidHostLabel(hostLabel, Boolean.TRUE.equals(allowDots)) + ? Boolean.TRUE + : Boolean.FALSE; + sp = idx + 1; + } + case Opcodes.PARSE_URL -> { + int idx = sp - 1; + var urlString = (String) stack[idx]; + stack[idx] = urlString == null ? null : uriFactory.createUri(urlString); + } + case Opcodes.URI_ENCODE -> { + int idx = sp - 1; + var string = (String) stack[idx]; + stack[idx] = URLEncoding.encodeUnreserved(string, false); + } + case Opcodes.RETURN_ERROR -> throw new RulesEvaluationError((String) stack[--sp], pc); + case Opcodes.RETURN_ENDPOINT -> { + var packed = instructions[pc++]; + boolean hasHeaders = (packed & 1) != 0; + boolean hasProperties = (packed & 2) != 0; + var urlValue = stack[--sp]; + var properties = (Map) (hasProperties ? stack[--sp] : Map.of()); + var headers = (Map>) (hasHeaders ? stack[--sp] : Map.of()); + // 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); + } + for (var extension : extensions) { + extension.extractEndpointProperties(builder, context, properties, headers); + } + return builder.build(); + } + case Opcodes.RETURN_VALUE -> { + return stack[--sp]; + } + case Opcodes.JNN_OR_POP -> { + Object value = stack[sp - 1]; + // Read as unsigned 16-bit value (0-65535) + int offset = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + if (value != null) { + pc += offset; // Jump forward, keeping value on stack + } else { + sp--; // Pop the null value + } + } + case Opcodes.SPLIT -> { + int idx = sp - 3; + var string = (String) stack[idx]; + var delimiter = (String) stack[idx + 1]; + var limit = ((Number) stack[idx + 2]).intValue(); + stack[idx] = Split.split(string, delimiter, limit); + sp = idx + 1; + } + case Opcodes.GET_NEGATIVE_INDEX -> { + int index = instructions[pc++] & 0xFF; + int idx = sp - 1; + stack[idx] = EndpointUtils.getNegativeIndex(stack[idx], index); + } + case Opcodes.GET_NEGATIVE_INDEX_REG -> { + int regIndex = instructions[pc++] & 0xFF; + int index = instructions[pc++] & 0xFF; + stack[sp++] = EndpointUtils.getNegativeIndex(regs[regIndex], index); + } + case Opcodes.JMP_IF_FALSE -> { + Object condition = stack[--sp]; + // Read as unsigned 16-bit value (0-65535) + int offset = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + if (condition != Boolean.TRUE) { + pc += offset; + } + } + case Opcodes.JUMP -> { + // Read as unsigned 16-bit value (0-65535) + int offset = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; pc += offset; } + case Opcodes.SUBSTRING_EQ -> { + int seqRegIndex = instructions[pc++] & 0xFF; + int seqStart = instructions[pc++] & 0xFF; + int seqEnd = instructions[pc++] & 0xFF; + int seqFlags = instructions[pc++] & 0xFF; + int seqConstIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + boolean seqReverse = (seqFlags & 0x01) != 0; + var seqValue = (String) regs[seqRegIndex]; + var seqExpected = (String) constantPool[seqConstIdx]; + stack[sp++] = EndpointUtils.substringEquals(seqValue, seqStart, seqEnd, seqReverse, seqExpected) + ? Boolean.TRUE + : Boolean.FALSE; + } + case Opcodes.SPLIT_GET -> { + int sgRegIndex = instructions[pc++] & 0xFF; + int sgDelimIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + int sgIndex = instructions[pc++]; // signed byte + var sgValue = (String) regs[sgRegIndex]; + var sgDelimiter = (String) constantPool[sgDelimIdx]; + stack[sp++] = EndpointUtils.splitGet(sgValue, sgDelimiter, sgIndex); + } + case Opcodes.SELECT_BOOL_REG -> { + var selVal = regs[instructions[pc++] & 0xFF]; + int selTrue = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + int selFalse = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + stack[sp++] = selVal != null && selVal != Boolean.FALSE + ? constantPool[selTrue] + : constantPool[selFalse]; + } + 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) regs[srcRegIndex]; + var srcExpected = (String) constantPool[srcConstIdx]; + stack[sp++] = srcValue != null && srcValue.equals(srcExpected) ? Boolean.TRUE : Boolean.FALSE; + } + case Opcodes.SET_REG_RETURN -> { + int srIndex = instructions[pc++] & 0xFF; + Object srValue = stack[--sp]; + regs[srIndex] = srValue; + return srValue; + } + case Opcodes.BUILD_URI -> { + int schemeIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); + pc += 2; + int idx = sp - 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; + } + sp = idx + 1; + } + default -> throw new RulesEvaluationError("Unknown rules engine instruction: " + opcode, pc); } - case Opcodes.JUMP -> { - // Read as unsigned 16-bit value (0-65535) - int offset = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - pc += offset; - } - case Opcodes.SUBSTRING_EQ -> { - int seqRegIndex = instructions[pc++] & 0xFF; - int seqStart = instructions[pc++] & 0xFF; - int seqEnd = instructions[pc++] & 0xFF; - int seqFlags = instructions[pc++] & 0xFF; - int seqConstIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - boolean seqReverse = (seqFlags & 0x01) != 0; - var seqValue = (String) registers[seqRegIndex]; - var seqExpected = (String) constantPool[seqConstIdx]; - push(EndpointUtils.substringEquals(seqValue, seqStart, seqEnd, seqReverse, seqExpected) - ? Boolean.TRUE - : Boolean.FALSE); - } - case Opcodes.SPLIT_GET -> { - int sgRegIndex = instructions[pc++] & 0xFF; - int sgDelimIdx = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - int sgIndex = instructions[pc++]; // signed byte - var sgValue = (String) registers[sgRegIndex]; - var sgDelimiter = (String) constantPool[sgDelimIdx]; - push(EndpointUtils.splitGet(sgValue, sgDelimiter, sgIndex)); - } - case Opcodes.SELECT_BOOL_REG -> { - var selVal = registers[instructions[pc++] & 0xFF]; - int selTrue = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - int selFalse = ((instructions[pc] & 0xFF) << 8) | (instructions[pc + 1] & 0xFF); - pc += 2; - push(selVal != null && selVal != Boolean.FALSE - ? constantPool[selTrue] - : constantPool[selFalse]); - } - 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"); + throw new IllegalArgumentException("Expected to return a value during evaluation"); + } finally { + this.pc = pc; + this.stackPosition = sp; + } } }