Skip to content
Open
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 @@ -98,6 +98,7 @@
* 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.
* 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.
* LAL: support full arithmetic (`+`, `-`, `*`, `/`) on numeric operands. Operand types are 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)` now compiles to `int + int` rather than being 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 now accepted in `typeCast` clauses.

#### 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 @@ -66,6 +66,8 @@ AS: 'as';
STRING_TYPE: 'String';
LONG_TYPE: 'Long';
INTEGER_TYPE: 'Integer';
DOUBLE_TYPE: 'Double';
FLOAT_TYPE: 'Float';
BOOLEAN_TYPE: 'Boolean';

// Keywords - built-in references
Expand Down Expand Up @@ -111,8 +113,17 @@ TRUE: 'true';
FALSE: 'false';
NULL: 'null';

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

// String literal: single or double quoted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,19 @@ conditionExpr
// tag("LOG_KIND")
// ProcessRegistry.generateVirtualLocalProcess(...)

// Arithmetic / string-concat expressions. Operator precedence: * / bind tighter than + -.
// All four operators share the same flattened model in the AST: each level produces a
// list of operands plus a list of operators (size N-1).
valueAccess
: valueAccessTerm (PLUS valueAccessTerm)*
: valueAccessAdd
;

valueAccessAdd
: valueAccessMul ((PLUS | MINUS) valueAccessMul)*
;

valueAccessMul
: valueAccessTerm ((STAR | SLASH) valueAccessTerm)*
;

valueAccessTerm
Expand Down Expand Up @@ -374,7 +385,7 @@ functionArg
// ==================== Type cast ====================

typeCast
: AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | BOOLEAN_TYPE | qualifiedName)
: AS (STRING_TYPE | LONG_TYPE | INTEGER_TYPE | DOUBLE_TYPE | FLOAT_TYPE | BOOLEAN_TYPE | qualifiedName)
;

qualifiedName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ private static Class<?> resolveDefCastType(final String castType) {
return Long.class;
case "Integer":
return Integer.class;
case "Double":
return Double.class;
case "Float":
return Float.class;
case "Boolean":
return Boolean.class;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,17 @@ public ExprCondition(final ValueAccess expr, final String castType) {

// ==================== Value access ====================

/**
* Binary arithmetic / concat operator joining adjacent {@code concatParts}.
* The list of operators aligns with the gaps between parts: for N parts there
* are N-1 operators, where {@code concatOps[i]} joins {@code concatParts[i]}
* and {@code concatParts[i + 1]}. Precedence is already encoded by the parser
* (mul-level expressions sit inside add-level parts).
*/
public enum BinaryOp {
PLUS, MINUS, STAR, SLASH
}

@Getter
public static final class ValueAccess {
private final List<String> segments;
Expand All @@ -359,6 +370,7 @@ public static final class ValueAccess {
private final String functionCallName;
private final List<FunctionArg> functionCallArgs;
private final List<ValueAccess> concatParts;
private final List<BinaryOp> concatOps;
private final ValueAccess parenInner;
private final String parenCast;

Expand All @@ -368,7 +380,7 @@ public ValueAccess(final List<String> segments,
final List<ValueAccessSegment> chain) {
this(segments, parsedRef, logRef, false, false, false,
chain, null, Collections.emptyList(),
Collections.emptyList(), null, null);
Collections.emptyList(), Collections.emptyList(), null, null);
}

public ValueAccess(final List<String> segments,
Expand All @@ -383,7 +395,7 @@ public ValueAccess(final List<String> segments,
this(segments, parsedRef, logRef, processRegistryRef,
stringLiteral, numberLiteral, chain,
functionCallName, functionCallArgs,
Collections.emptyList(), null, null);
Collections.emptyList(), Collections.emptyList(), null, null);
}

public ValueAccess(final List<String> segments,
Expand All @@ -398,6 +410,26 @@ public ValueAccess(final List<String> segments,
final List<ValueAccess> concatParts,
final ValueAccess parenInner,
final String parenCast) {
this(segments, parsedRef, logRef, processRegistryRef,
stringLiteral, numberLiteral, chain,
functionCallName, functionCallArgs,
concatParts, defaultPlusOps(concatParts),
parenInner, parenCast);
}

public ValueAccess(final List<String> segments,
final boolean parsedRef,
final boolean logRef,
final boolean processRegistryRef,
final boolean stringLiteral,
final boolean numberLiteral,
final List<ValueAccessSegment> chain,
final String functionCallName,
final List<FunctionArg> functionCallArgs,
final List<ValueAccess> concatParts,
final List<BinaryOp> concatOps,
final ValueAccess parenInner,
final String parenCast) {
this.segments = Collections.unmodifiableList(segments);
this.parsedRef = parsedRef;
this.logRef = logRef;
Expand All @@ -411,13 +443,26 @@ public ValueAccess(final List<String> segments,
? Collections.unmodifiableList(functionCallArgs) : Collections.emptyList();
this.concatParts = concatParts != null
? Collections.unmodifiableList(concatParts) : Collections.emptyList();
this.concatOps = concatOps != null
? Collections.unmodifiableList(concatOps) : Collections.emptyList();
this.parenInner = parenInner;
this.parenCast = parenCast;
}

public String toPathString() {
return String.join(".", segments);
}

private static List<BinaryOp> defaultPlusOps(final List<ValueAccess> parts) {
if (parts == null || parts.size() <= 1) {
return Collections.emptyList();
}
final BinaryOp[] ops = new BinaryOp[parts.size() - 1];
for (int i = 0; i < ops.length; i++) {
ops[i] = BinaryOp.PLUS;
}
return List.of(ops);
}
}

@Getter
Expand Down Expand Up @@ -486,9 +531,22 @@ public StringConditionValue(final String value) {
@Getter
public static final class NumberConditionValue implements ConditionValue {
private final double value;
/**
* Original literal text from the LAL source (e.g. {@code "10000"},
* {@code "10000L"}, {@code "1.5"}, {@code "1e6"}). Codegen uses this
* to preserve the user-declared numeric type — a bare {@code 10000}
* stays {@code int}, an {@code L}-suffixed literal becomes {@code long},
* etc. May be {@code null} for synthesised values.
*/
private final String literal;

public NumberConditionValue(final double value) {
this(value, null);
}

public NumberConditionValue(final double value, final String literal) {
this.value = value;
this.literal = literal;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.apache.skywalking.lal.rt.grammar.LALLexer;
import org.apache.skywalking.lal.rt.grammar.LALParser;
import org.apache.skywalking.oap.log.analyzer.v2.compiler.LALScriptModel.AbortStatement;
Expand Down Expand Up @@ -528,8 +530,9 @@ private static LALScriptModel.ConditionValue visitConditionExprAsValue(
stripQuotes(((LALParser.CondStringContext) ctx).STRING().getText()));
}
if (ctx instanceof LALParser.CondNumberContext) {
return new NumberConditionValue(
Double.parseDouble(((LALParser.CondNumberContext) ctx).NUMBER().getText()));
final String numText =
((LALParser.CondNumberContext) ctx).NUMBER().getText();
return new NumberConditionValue(parseLiteralAsDouble(numText), numText);
}
if (ctx instanceof LALParser.CondNullContext) {
return new NullConditionValue();
Expand All @@ -541,13 +544,15 @@ private static LALScriptModel.ConditionValue visitConditionExprAsValue(
// (since valueAccessPrimary includes them and condValueAccess has priority).
// Detect standalone literals and create proper ConditionValue types.
final LALParser.ValueAccessContext vaCtx = va.valueAccess();
if (va.typeCast() == null && vaCtx.valueAccessTerm().size() == 1
&& vaCtx.valueAccessTerm(0).valueAccessSegment().isEmpty()) {
final LALParser.ValueAccessTermContext singleTerm = singleTermOf(vaCtx);
if (va.typeCast() == null && singleTerm != null
&& singleTerm.valueAccessSegment().isEmpty()) {
final LALParser.ValueAccessPrimaryContext primary =
vaCtx.valueAccessTerm(0).valueAccessPrimary();
singleTerm.valueAccessPrimary();
if (primary instanceof LALParser.ValueNumberContext) {
return new NumberConditionValue(Double.parseDouble(
((LALParser.ValueNumberContext) primary).NUMBER().getText()));
final String numText =
((LALParser.ValueNumberContext) primary).NUMBER().getText();
return new NumberConditionValue(parseLiteralAsDouble(numText), numText);
}
if (primary instanceof LALParser.ValueNullContext) {
return new NullConditionValue();
Expand Down Expand Up @@ -595,19 +600,62 @@ private static Condition visitConditionExprAsCondition(
// ==================== Value access ====================

private static ValueAccess visitValueAccess(final LALParser.ValueAccessContext ctx) {
return visitValueAccessAdd(ctx.valueAccessAdd());
}

private static ValueAccess visitValueAccessAdd(final LALParser.ValueAccessAddContext ctx) {
final List<LALParser.ValueAccessMulContext> muls = ctx.valueAccessMul();
if (muls.size() == 1) {
return visitValueAccessMul(muls.get(0));
}
final List<ValueAccess> parts = new ArrayList<>();
for (final LALParser.ValueAccessMulContext mul : muls) {
parts.add(visitValueAccessMul(mul));
}
// Walk children to recover operator order — PLUS/MINUS appear interleaved with mul nodes.
final List<LALScriptModel.BinaryOp> ops = new ArrayList<>(muls.size() - 1);
for (int i = 0; i < ctx.getChildCount(); i++) {
final ParseTree child = ctx.getChild(i);
if (child instanceof TerminalNode) {
final int type = ((TerminalNode) child).getSymbol().getType();
if (type == LALLexer.PLUS) {
ops.add(LALScriptModel.BinaryOp.PLUS);
} else if (type == LALLexer.MINUS) {
ops.add(LALScriptModel.BinaryOp.MINUS);
}
}
}
return new ValueAccess(
List.of("expr"), false, false, false, false, false,
List.of(), null, null,
parts, ops, null, null);
}

private static ValueAccess visitValueAccessMul(final LALParser.ValueAccessMulContext ctx) {
final List<LALParser.ValueAccessTermContext> terms = ctx.valueAccessTerm();
if (terms.size() == 1) {
return visitValueAccessTerm(terms.get(0));
}
// Multiple terms joined by PLUS — string concatenation
final List<ValueAccess> parts = new ArrayList<>();
for (final LALParser.ValueAccessTermContext term : terms) {
parts.add(visitValueAccessTerm(term));
}
final List<LALScriptModel.BinaryOp> ops = new ArrayList<>(terms.size() - 1);
for (int i = 0; i < ctx.getChildCount(); i++) {
final ParseTree child = ctx.getChild(i);
if (child instanceof TerminalNode) {
final int type = ((TerminalNode) child).getSymbol().getType();
if (type == LALLexer.STAR) {
ops.add(LALScriptModel.BinaryOp.STAR);
} else if (type == LALLexer.SLASH) {
ops.add(LALScriptModel.BinaryOp.SLASH);
}
}
}
return new ValueAccess(
List.of("concat"), false, false, false, false, false,
List.of("expr"), false, false, false, false, false,
List.of(), null, null,
parts, null, null);
parts, ops, null, null);
}

private static ValueAccess visitValueAccessTerm(
Expand Down Expand Up @@ -736,14 +784,50 @@ private static List<LALScriptModel.FunctionArg> visitFunctionArgs(
}

private static String resolveValueAsString(final LALParser.ValueAccessContext ctx) {
final LALParser.ValueAccessPrimaryContext primary =
ctx.valueAccessTerm(0).valueAccessPrimary();
final LALParser.ValueAccessTermContext term = singleTermOf(ctx);
if (term == null) {
return ctx.getText();
}
final LALParser.ValueAccessPrimaryContext primary = term.valueAccessPrimary();
if (primary instanceof LALParser.ValueStringContext) {
return stripQuotes(((LALParser.ValueStringContext) primary).STRING().getText());
}
return primary.getText();
}

/**
* Returns the single {@link LALParser.ValueAccessTermContext} of a
* {@code valueAccess} when it has no arithmetic operators, otherwise null.
*/
private static LALParser.ValueAccessTermContext singleTermOf(
final LALParser.ValueAccessContext ctx) {
final LALParser.ValueAccessAddContext add = ctx.valueAccessAdd();
if (add.valueAccessMul().size() != 1) {
return null;
}
final LALParser.ValueAccessMulContext mul = add.valueAccessMul(0);
if (mul.valueAccessTerm().size() != 1) {
return null;
}
return mul.valueAccessTerm(0);
}

/**
* Parse a NUMBER literal text (which may carry an L/F/D suffix) as a Java double
* for use in {@link NumberConditionValue}. Suffix is stripped before parsing.
*/
private static double parseLiteralAsDouble(final String numText) {
String t = numText;
if (!t.isEmpty()) {
final char last = t.charAt(t.length() - 1);
if (last == 'L' || last == 'l' || last == 'F' || last == 'f'
|| last == 'D' || last == 'd') {
t = t.substring(0, t.length() - 1);
}
}
return Double.parseDouble(t);
}

// ==================== Utilities ====================

private static String extractCastType(final LALParser.TypeCastContext ctx) {
Expand All @@ -756,6 +840,12 @@ private static String extractCastType(final LALParser.TypeCastContext ctx) {
if (ctx.INTEGER_TYPE() != null) {
return "Integer";
}
if (ctx.DOUBLE_TYPE() != null) {
return "Double";
}
if (ctx.FLOAT_TYPE() != null) {
return "Float";
}
if (ctx.BOOLEAN_TYPE() != null) {
return "Boolean";
}
Expand Down
Loading
Loading