close
Skip to content

Commit 67160de

Browse files
authored
LAL: unify arithmetic operators (+ - * /) with JLS-style type promotion (#13858)
1 parent 2b745b2 commit 67160de

10 files changed

Lines changed: 2323 additions & 273 deletions

File tree

‎docs/en/changes/changes.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@
9696
* Fix: potential unexpected current directory inclusion in Docker OAP classpath.
9797
* 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.
9898
* Fix: `envoy-ai-gateway` metrics rules, make the metrics value return `0` when the divisor is `0`.
99-
* 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.
10099
* Custom `Layer`s can be declared without modifying the OAP source — via an operator-managed `layer-extensions.yml`, inline `layerDefinitions:` block in a MAL or LAL rule file, or a plugin extension. UI dashboard templates for new layers are auto-discovered from the `ui-initialized-templates/` directory. Recommended ordinal range for external layers is `>= 1000`; conflicting names or ordinals are reported at boot.
100+
* LAL: support full arithmetic (`+`, `-`, `*`, `/`) on numeric operands and fix the original bug where `(tag("x") as Integer) + (tag("y") as Integer)` was treated as string concatenation — 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 {}`. Operand types are now inferred from explicit casts (`as Integer` / `as Long` / `as Float` / `as Double`), typed proto fields, or numeric literal shape (with `L` / `F` / `D` suffix support, e.g. `1000L`). The compiler honours JLS-style binary numeric promotion and emits Java arithmetic in the declared primitive type — `(x as Integer) + (y as Integer)` compiles to `int + int` (not widened to `long`). `+` with any String operand falls back to string concatenation; `-` / `*` / `/` against non-numeric operands produces a compile-time error. The `as Double` and `as Float` casts are accepted in `typeCast` clauses, including in `def` declarations. Numeric comparisons honour declared casts on both sides (no more universal `h.toLong()` wrapper).
101101

102102
#### UI
103103
* Add mobile menu icon and i18n labels for the iOS layer.

‎oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALLexer.g4‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ AS: 'as';
6666
STRING_TYPE: 'String';
6767
LONG_TYPE: 'Long';
6868
INTEGER_TYPE: 'Integer';
69+
DOUBLE_TYPE: 'Double';
70+
FLOAT_TYPE: 'Float';
6971
BOOLEAN_TYPE: 'Boolean';
7072

7173
// Keywords - built-in references
@@ -111,8 +113,17 @@ TRUE: 'true';
111113
FALSE: 'false';
112114
NULL: 'null';
113115

116+
// Numeric literal. Suffix rules mirror Java:
117+
// * `L` / `l` is valid only on integer literals (no decimal, no exponent).
118+
// * `F` / `f` and `D` / `d` are valid on any form.
119+
// Bare integer literals (no suffix, no decimal, no exponent) are interpreted as int
120+
// by the compiler if they fit, otherwise long. Bare decimal/exponent literals are
121+
// double unless suffixed.
114122
NUMBER
115-
: Digit+ ('.' Digit+)?
123+
: Digit+ '.' Digit+ ([eE] [+\-]? Digit+)? [FfDd]?
124+
| Digit+ [eE] [+\-]? Digit+ [FfDd]?
125+
| '.' Digit+ ([eE] [+\-]? Digit+)? [FfDd]?
126+
| Digit+ [LlFfDd]?
116127
;
117128

118129
// String literal: single or double quoted

‎oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,19 @@ conditionExpr
321321
// tag("LOG_KIND")
322322
// ProcessRegistry.generateVirtualLocalProcess(...)
323323

324+
// Arithmetic / string-concat expressions. Operator precedence: * / bind tighter than + -.
325+
// All four operators share the same flattened model in the AST: each level produces a
326+
// list of operands plus a list of operators (size N-1).
324327
valueAccess
325-
: valueAccessTerm (PLUS valueAccessTerm)*
328+
: valueAccessAdd
329+
;
330+
331+
valueAccessAdd
332+
: valueAccessMul ((PLUS | MINUS) valueAccessMul)*
333+
;
334+
335+
valueAccessMul
336+
: valueAccessTerm ((STAR | SLASH) valueAccessTerm)*
326337
;
327338

328339
valueAccessTerm
@@ -374,7 +385,7 @@ functionArg
374385
// ==================== Type cast ====================
375386

376387
typeCast
377-
: AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | BOOLEAN_TYPE | qualifiedName)
388+
: AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | DOUBLE_TYPE | FLOAT_TYPE | BOOLEAN_TYPE | qualifiedName)
378389
;
379390

380391
qualifiedName

‎oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALDefCodegen.java‎

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,51 @@ static void generateDefStatement(final StringBuilder sb,
148148
});
149149
}
150150

151-
// Emit assignment in body (at the point where def appears)
151+
// Emit assignment in body (at the point where def appears).
152+
// For built-in scalar casts the initialiser usually returns String
153+
// or Object (e.g. tag() returns String), so a plain Java cast would
154+
// throw ClassCastException at runtime. Route through the runtime
155+
// helper which performs the proper conversion. FQCN casts still use
156+
// a direct Java cast — those name a concrete type the caller knows
157+
// the initialiser already produces.
152158
sb.append(" ").append(javaVar).append(" = ");
153159
if (castType != null && !castType.isEmpty()) {
154-
sb.append("(").append(resolvedType.getName()).append(") ");
160+
final String conversion = scalarCastConversion(castType, initExpr);
161+
if (conversion != null) {
162+
sb.append(conversion);
163+
} else {
164+
sb.append("(").append(resolvedType.getName()).append(") ").append(initExpr);
165+
}
166+
} else {
167+
sb.append(initExpr);
168+
}
169+
sb.append(";\n");
170+
}
171+
172+
/**
173+
* Render the boxed runtime-helper conversion for a scalar cast type
174+
* (e.g. {@code Long.valueOf(h.toLong(<init>))}). Returns {@code null}
175+
* for FQCN / unknown cast types so the caller can fall back to a
176+
* direct Java cast.
177+
*/
178+
private static String scalarCastConversion(final String castType,
179+
final CharSequence init) {
180+
switch (castType) {
181+
case "String":
182+
return "h.toStr(" + init + ")";
183+
case "Long":
184+
return "Long.valueOf(h.toLong(" + init + "))";
185+
case "Integer":
186+
return "Integer.valueOf(h.toInt(" + init + "))";
187+
case "Double":
188+
return "Double.valueOf(h.toDouble(" + init + "))";
189+
case "Float":
190+
return "Float.valueOf(h.toFloat(" + init + "))";
191+
case "Boolean":
192+
return "Boolean.valueOf(h.toBool(" + init + "))";
193+
default:
194+
return null;
155195
}
156-
sb.append(initExpr).append(";\n");
157196
}
158197

159198
/**
@@ -169,6 +208,10 @@ private static Class<?> resolveDefCastType(final String castType) {
169208
return Long.class;
170209
case "Integer":
171210
return Integer.class;
211+
case "Double":
212+
return Double.class;
213+
case "Float":
214+
return Float.class;
172215
case "Boolean":
173216
return Boolean.class;
174217
default:

‎oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptModel.java‎

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,32 @@ public ExprCondition(final ValueAccess expr, final String castType) {
347347

348348
// ==================== Value access ====================
349349

350+
/**
351+
* Binary arithmetic / concat operator joining adjacent {@code concatParts}.
352+
* The list of operators aligns with the gaps between parts: for N parts there
353+
* are N-1 operators, where {@code concatOps[i]} joins {@code concatParts[i]}
354+
* and {@code concatParts[i + 1]}. Precedence is already encoded by the parser
355+
* (mul-level expressions sit inside add-level parts). The {@code symbol}
356+
* field carries the Java source token so the codegen doesn't need a parallel
357+
* switch.
358+
*/
359+
public enum BinaryOp {
360+
PLUS("+"),
361+
MINUS("-"),
362+
STAR("*"),
363+
SLASH("/");
364+
365+
private final String symbol;
366+
367+
BinaryOp(final String symbol) {
368+
this.symbol = symbol;
369+
}
370+
371+
public String symbol() {
372+
return symbol;
373+
}
374+
}
375+
350376
@Getter
351377
public static final class ValueAccess {
352378
private final List<String> segments;
@@ -359,6 +385,7 @@ public static final class ValueAccess {
359385
private final String functionCallName;
360386
private final List<FunctionArg> functionCallArgs;
361387
private final List<ValueAccess> concatParts;
388+
private final List<BinaryOp> concatOps;
362389
private final ValueAccess parenInner;
363390
private final String parenCast;
364391

@@ -368,7 +395,7 @@ public ValueAccess(final List<String> segments,
368395
final List<ValueAccessSegment> chain) {
369396
this(segments, parsedRef, logRef, false, false, false,
370397
chain, null, Collections.emptyList(),
371-
Collections.emptyList(), null, null);
398+
Collections.emptyList(), Collections.emptyList(), null, null);
372399
}
373400

374401
public ValueAccess(final List<String> segments,
@@ -383,7 +410,26 @@ public ValueAccess(final List<String> segments,
383410
this(segments, parsedRef, logRef, processRegistryRef,
384411
stringLiteral, numberLiteral, chain,
385412
functionCallName, functionCallArgs,
386-
Collections.emptyList(), null, null);
413+
Collections.emptyList(), Collections.emptyList(), null, null);
414+
}
415+
416+
public ValueAccess(final List<String> segments,
417+
final boolean parsedRef,
418+
final boolean logRef,
419+
final boolean processRegistryRef,
420+
final boolean stringLiteral,
421+
final boolean numberLiteral,
422+
final List<ValueAccessSegment> chain,
423+
final String functionCallName,
424+
final List<FunctionArg> functionCallArgs,
425+
final List<ValueAccess> concatParts,
426+
final ValueAccess parenInner,
427+
final String parenCast) {
428+
this(segments, parsedRef, logRef, processRegistryRef,
429+
stringLiteral, numberLiteral, chain,
430+
functionCallName, functionCallArgs,
431+
concatParts, defaultPlusOps(concatParts),
432+
parenInner, parenCast);
387433
}
388434

389435
public ValueAccess(final List<String> segments,
@@ -396,6 +442,7 @@ public ValueAccess(final List<String> segments,
396442
final String functionCallName,
397443
final List<FunctionArg> functionCallArgs,
398444
final List<ValueAccess> concatParts,
445+
final List<BinaryOp> concatOps,
399446
final ValueAccess parenInner,
400447
final String parenCast) {
401448
this.segments = Collections.unmodifiableList(segments);
@@ -411,13 +458,26 @@ public ValueAccess(final List<String> segments,
411458
? Collections.unmodifiableList(functionCallArgs) : Collections.emptyList();
412459
this.concatParts = concatParts != null
413460
? Collections.unmodifiableList(concatParts) : Collections.emptyList();
461+
this.concatOps = concatOps != null
462+
? Collections.unmodifiableList(concatOps) : Collections.emptyList();
414463
this.parenInner = parenInner;
415464
this.parenCast = parenCast;
416465
}
417466

418467
public String toPathString() {
419468
return String.join(".", segments);
420469
}
470+
471+
private static List<BinaryOp> defaultPlusOps(final List<ValueAccess> parts) {
472+
if (parts == null || parts.size() <= 1) {
473+
return Collections.emptyList();
474+
}
475+
final BinaryOp[] ops = new BinaryOp[parts.size() - 1];
476+
for (int i = 0; i < ops.length; i++) {
477+
ops[i] = BinaryOp.PLUS;
478+
}
479+
return List.of(ops);
480+
}
421481
}
422482

423483
@Getter
@@ -486,9 +546,22 @@ public StringConditionValue(final String value) {
486546
@Getter
487547
public static final class NumberConditionValue implements ConditionValue {
488548
private final double value;
549+
/**
550+
* Original literal text from the LAL source (e.g. {@code "10000"},
551+
* {@code "10000L"}, {@code "1.5"}, {@code "1e6"}). Codegen uses this
552+
* to preserve the user-declared numeric type — a bare {@code 10000}
553+
* stays {@code int}, an {@code L}-suffixed literal becomes {@code long},
554+
* etc. May be {@code null} for synthesised values.
555+
*/
556+
private final String literal;
489557

490558
public NumberConditionValue(final double value) {
559+
this(value, null);
560+
}
561+
562+
public NumberConditionValue(final double value, final String literal) {
491563
this.value = value;
564+
this.literal = literal;
492565
}
493566
}
494567

0 commit comments

Comments
 (0)