Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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) (h.toInt(...) + h.toInt(...))
Comment thread
wankai123 marked this conversation as resolved.
Outdated
* }</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