Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/changes/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
* Fix: potential unexpected current directory inclusion in Docker OAP classpath.
* MAL: add `safeDiv(divisor)` on `SampleFamily` that yields `0` when the divisor is `0` instead of `Infinity`/`NaN`. Replace `/` with `safeDiv(...)` in Envoy AI Gateway latency-average rules so `sum / count * 1000` no longer produces dropped or out-of-range samples when a counter is zero in a window.
* Fix: `envoy-ai-gateway` metrics rules, make the metrics value return `0` when the divisor is `0`.
* Fix: LAL compiler treated `(tag("x") as Integer) + (tag("y") as Integer)` as string concatenation instead of numeric addition. Expressions like `input_tokens + output_tokens < 10000` produced the concatenated string `"2589115"` rather than the integer sum `2704`, so token-threshold conditions never triggered `abort {}`. The compiler now detects all-numeric operands (cast to `Integer` or `Long`) and emits proper `long` arithmetic.

#### UI
* Add mobile menu icon and i18n labels for the iOS layer.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
* def variable myVar?.getAsString() _def_myVar?.getAsString()
* Parenthesized (expr as String).trim() h.toStr(...).trim()
* String concat "${log.service}:${parsed.code}" "" + ... + ":" + ...
* Arithmetic sum (tag("a") as Integer) + (tag("b") as Integer) Long.valueOf((long) h.toInt(...) + (long) h.toInt(...))
* }</pre>
*
* <p>Condition codegen ({@link #generateCondition}) handles {@code if}
Expand Down Expand Up @@ -342,16 +343,22 @@ static void generateValueAccess(final StringBuilder sb,
final LALClassGenerator.GenCtx genCtx) {
genCtx.clearExtraLogResult();

// Handle string concatenation (term1 + term2 + ...)
// Handle string concatenation or arithmetic addition (term1 + term2 + ...)
// When every part is a numeric cast (Integer/Long) or a number literal,
// emit integer/long arithmetic. Otherwise emit string concatenation.
if (!value.getConcatParts().isEmpty()) {
sb.append("(\"\" + ");
for (int i = 0; i < value.getConcatParts().size(); i++) {
if (i > 0) {
sb.append(" + ");
if (allPartsNumeric(value.getConcatParts())) {
generateArithmeticSum(sb, value.getConcatParts(), genCtx);
} else {
sb.append("(\"\" + ");
for (int i = 0; i < value.getConcatParts().size(); i++) {
if (i > 0) {
sb.append(" + ");
}
generateValueAccess(sb, value.getConcatParts().get(i), genCtx);
}
generateValueAccess(sb, value.getConcatParts().get(i), genCtx);
sb.append(")");
}
sb.append(")");
return;
}

Expand Down Expand Up @@ -858,6 +865,82 @@ static void generateProcessRegistryCall(

// ==================== Utility methods ====================

/**
* Returns {@code true} when every concat part is numeric — i.e. a
* parenthesized expression with an {@code Integer} or {@code Long} cast,
* or a bare number literal. If so, {@code +} is arithmetic, not string
* concatenation.
*/
private static boolean allPartsNumeric(final List<LALScriptModel.ValueAccess> parts) {
for (final LALScriptModel.ValueAccess part : parts) {
if (part.isNumberLiteral()) {
continue;
}
if (part.getParenInner() != null && isNumericCast(part.getParenCast())) {
continue;
}
return false;
}
Comment thread
wankai123 marked this conversation as resolved.
return true;
}

private static boolean isNumericCast(final String cast) {
return "Integer".equals(cast) || "Long".equals(cast);
}

/**
* Generates an arithmetic sum expression for a list of numeric parts.
* Always uses {@code long} arithmetic to avoid Javassist autoboxing
* restrictions (Javassist cannot pass a primitive {@code int/long} to a
* method that expects {@code Object}, e.g. {@code h.toLong(int)}).
*
* <p>Two outputs are produced:
* <ul>
* <li>The raw {@code long} expression is stored in
* {@code genCtx.lastRawChain} so that {@code generateNumericComparison}
* can emit a direct primitive comparison without a
* {@code h.toLong()} wrapper.</li>
* <li>{@code Long.valueOf(rawExpr)} is appended to {@code sb} so that
* non-comparison contexts (tag assignment, {@code h.toStr()}, etc.)
* receive a boxed {@code Long} — a valid {@code Object}.</li>
* </ul>
*
* <p>Examples:
* <pre>{@code
* (tag("a") as Integer) + (tag("b") as Integer) < 10000
* rawExpr → ((long) h.toInt(h.tagValue("a")) + (long) h.toInt(h.tagValue("b")))
* in sb → Long.valueOf(((long) h.toInt(...) + (long) h.toInt(...)))
* comparison emits → rawExpr < 10000L (via lastRawChain / primitiveNumeric path)
* }</pre>
*/
private static void generateArithmeticSum(final StringBuilder sb,
final List<LALScriptModel.ValueAccess> parts,
final LALClassGenerator.GenCtx genCtx) {
final StringBuilder expr = new StringBuilder("(");
for (int i = 0; i < parts.size(); i++) {
if (i > 0) {
expr.append(" + ");
}
final LALScriptModel.ValueAccess part = parts.get(i);
if (part.isNumberLiteral()) {
expr.append(part.getSegments().get(0)).append("L");
} else if ("Long".equals(part.getParenCast())) {
Comment thread
wankai123 marked this conversation as resolved.
generateCastedValueAccess(expr, part.getParenInner(), "Long", genCtx);
} else {
expr.append("(long) ");
generateCastedValueAccess(expr, part.getParenInner(), "Integer", genCtx);
}
}
expr.append(")");
final String rawExpr = expr.toString();
// Let generateNumericComparison skip h.toLong() and compare directly.
genCtx.lastResolvedType = long.class;
genCtx.lastRawChain = rawExpr;
genCtx.lastNullChecks = null;
// Box for Object contexts (h.toStr, h.toLong, def assignments, etc.)
sb.append("Long.valueOf(").append(rawExpr).append(")");
}

/**
* Appends a method call segment to the current expression chain.
* Handles safe-navigation ({@code ?.method()}) by wrapping in a null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
Expand Down Expand Up @@ -292,6 +293,66 @@ void compileAndVerifyElseIfEmitsNestedBranches() throws Exception {
+ ifCount + " in: " + source);
}

// ==================== Arithmetic addition in conditions ====================

@Test
void compileArithmeticSumOfIntegerTagsEmitsLongArithmetic() throws Exception {
// The envoy-ai-gateway token check pattern:
// (tag("input_tokens") as Integer) + (tag("output_tokens") as Integer) < 10000
// must do numeric addition (3033 < 10000 = true → abort),
// not string concat ("2872161" → 2872161 >= 10000 = false → no abort).
final String dsl = "filter {\n"
+ " if ((tag(\"a\") as Integer) + (tag(\"b\") as Integer) < 10000) {\n"
+ " abort {}\n"
+ " }\n"
+ " sink {}\n"
+ "}";
compileAndAssert(dsl);
final String source = generator.generateSource(dsl);
assertTrue(source.contains("(long)"),
"Expected (long) promotion for Integer parts but got: " + source);
assertTrue(source.contains("h.toInt("),
"Expected h.toInt() for Integer cast but got: " + source);
assertFalse(source.contains("\"\" +"),
"Expected arithmetic addition, not string concat, but got: " + source);
// In a comparison context generateNumericComparison uses lastRawChain directly,
// so the comparison emits the raw long expression without Long.valueOf().
assertTrue(source.contains("< 10000L"),
"Expected '< 10000L' numeric comparison but got: " + source);
}

@Test
void compileArithmeticSumOfLongAndIntegerTagsEmitsLongArithmetic() throws Exception {
final String dsl = "filter {\n"
+ " if ((tag(\"a\") as Long) + (tag(\"b\") as Integer) < 10000) {\n"
+ " abort {}\n"
+ " }\n"
+ " sink {}\n"
+ "}";
compileAndAssert(dsl);
final String source = generator.generateSource(dsl);
assertTrue(source.contains("h.toLong("),
"Expected h.toLong() for Long cast but got: " + source);
assertTrue(source.contains("(long)"),
"Expected (long) promotion for Integer parts but got: " + source);
assertFalse(source.contains("\"\" +"),
"Expected arithmetic addition, not string concat, but got: " + source);
}

@Test
void compileStringConcatWithPlusRemainsStringConcat() throws Exception {
final String dsl = "filter {\n"
+ " extractor {\n"
+ " tag 'key': tag(\"a\") + tag(\"b\")\n"
+ " }\n"
+ " sink {}\n"
+ "}";
compileAndAssert(dsl);
final String source = generator.generateSource(dsl);
assertTrue(source.contains("\"\" +"),
"Expected string concatenation for uncast tags but got: " + source);
}

// ==================== sourceAttribute() function ====================

@Test
Expand Down
Loading