LAL: unify arithmetic operators (+ - * /) with JLS-style type promotion#13858
LAL: unify arithmetic operators (+ - * /) with JLS-style type promotion#13858
Conversation
…otion
Extend LAL value-access grammar to support full arithmetic — `+`, `-`, `*`, `/`
with proper precedence. Operand types are inferred from explicit casts (`as Integer`
/ `as Long` / `as Float` / `as Double`), typed proto fields, or numeric literal
shape. The compiler honours JLS-style binary numeric promotion and emits Java
arithmetic in the declared primitive type instead of forcing all integer
arithmetic through `long`.
`(x as Integer) + (y as Integer)` now compiles to `int + int` rather than being
widened defensively. `+` with any String operand falls back to string
concatenation; `-` / `*` / `/` against non-numeric operands produces a
compile-time error.
Numeric literals support Java-style suffixes — `1000L` (long), `1.5f` (float),
`1e3` (double) — with `L` valid only on integer literals (no decimal, no
exponent). The existing `typeCast` clause now accepts `Double` and `Float` in
addition to `String` / `Long` / `Integer` / `Boolean`, including in `def`
variable declarations.
Numeric comparisons honour declared casts on both sides instead of routing
everything through `h.toLong()`. `tag("a") as Double < 1.5` compiles to
`h.toDouble(...) < 1.5`; `tag("a") as Integer < 1.5` compiles to
`(double) h.toInt(...) < 1.5`. Typed proto chains (`parsed?.x.y as Integer`)
keep their direct unboxed comparison path.
There was a problem hiding this comment.
Pull request overview
This PR extends the LAL compiler to parse and generate all four arithmetic operators with Java-style numeric promotion, while adding Float/Double casts and richer numeric-literal handling. It sits in the log-analyzer’s LAL parser/codegen pipeline, touching grammar, AST modeling, runtime conversion helpers, and regression tests.
Changes:
- Expand the LAL grammar/model to represent
+,-,*,/with precedence and typed numeric literals/suffixes. - Rework value/comparison codegen to preserve declared primitive types, add
Float/Doublecasts, and emit promoted arithmetic/comparisons. - Add compiler regression tests and update the changelog for the new arithmetic behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
oap-server/analyzer/log-analyzer/src/test/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALClassGeneratorConditionTest.java |
Adds source-generation regression tests for arithmetic operators, promotions, literal suffixes, and casted comparisons. |
oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/rt/LalRuntimeHelper.java |
Introduces runtime coercion helpers for Double and Float casts. |
oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALValueCodegen.java |
Implements numeric type inference, promotion, arithmetic emission, and comparison literal formatting. |
oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptParser.java |
Parses additive/multiplicative expressions, preserves numeric literal text, and recognizes Float/Double casts. |
oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALScriptModel.java |
Extends the AST with binary operators and literal-preserving numeric condition values. |
oap-server/analyzer/log-analyzer/src/main/java/org/apache/skywalking/oap/log/analyzer/v2/compiler/LALDefCodegen.java |
Allows def declarations to resolve Float and Double cast types. |
oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALParser.g4 |
Updates parser grammar for operator precedence and new cast keywords. |
oap-server/analyzer/log-analyzer/src/main/antlr4/org/apache/skywalking/lal/rt/grammar/LALLexer.g4 |
Extends lexer NUMBER tokens to accept Java-style numeric suffixes and exponent forms. |
docs/en/changes/changes.md |
Documents the arithmetic/type-promotion enhancement in the changelog. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ed proto inference, exponent literal widening, changelog merge * Numeric comparisons preserve null guards on both sides. Previously the LHS guard was applied via ternary but the RHS guard from a typed-proto safe-nav chain was discarded, so `parsed?.x?.y < parsed?.a?.b` could throw NPE on an intermediate null instead of evaluating to false. Now the LHS and RHS metadata is captured separately and both guards feed the outer ternary as a disjunction. * `inferType` walks typed-proto chains via reflection when `inputType` is set. Uncast primitive numeric leaves (e.g. `parsed?.response?.code?.value`) now participate in arithmetic and comparisons instead of falling through to string concatenation or producing `requires numeric operands` errors on `-` / `*` / `/`. * `widenLiteralToType` no longer appends `.0` to exponent-form literals. Previously, widening `1e6` toward FLOAT/DOUBLE produced `1e6.0`, which is not valid Java. Bodies that contain `e` / `E` are already valid fractional literals and only need the suffix swap. * Changelog: fold the obsolete PR #13857 bullet into the new arithmetic bullet so the unreleased 10.5.0 section describes one consistent end-state instead of two contradictory ones.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…top-level cast narrowing, string-concat semantics, parser dedup
* def codegen routes scalar casts (Long/Integer/Double/Float/Boolean/String)
through the matching runtime helper (h.toLong / h.toInt / h.toDouble /
h.toFloat / h.toBool / h.toStr) instead of emitting a plain Java cast.
Previously `def x = tag("a") as Long` emitted `(Long) h.tagValue("a")`
which throws ClassCastException at runtime since tag() returns String.
* Strict-integer slots (`rateLimit { rpm N }` and `[index]`) reject
Java-style suffixed/decimal/exponent literals at parse time with a
clear error message instead of letting NumberFormatException leak from
Long.parseLong() / Integer.parseInt() at runtime.
* Top-level numeric cast on a comparison operand is honoured even when
the operand already produced a primitive — e.g.
`((tag("a") as Long) + 1) as Integer < 10` now narrows the long sum
back to int via a Java primitive cast, instead of silently keeping
the long value. Reads from `lastRawChain` (primitive) rather than the
boxed wrapper expression in the buffer to avoid `(int) Long.valueOf(...)`
which the bytecode verifier rejects.
* String-concat flattening preserves Java's left-to-right `+` semantics:
`1 + 2 + "x"` now compiles to `(1 + 2) + "x"` (= "3x") instead of the
prior `"" + 1 + 2 + "x"` (= "12x"). Single left-to-right walker now
handles both numeric arithmetic and string concat, transitioning to
String concat only when a String operand appears.
LALScriptParser refactor (per follow-up review):
* Dedupe `visitValueAccessAdd` / `visitValueAccessMul` operator-walking
via `collectInfixOps` and `binaryExpr` helpers.
* Extract `visitRateLimitBlock` to remove parsing duplication between
`visitSamplerBlock` and `visitIfBody`.
* Rename `makeComparison` → `visitComparison` for naming consistency
with the rest of the visitor methods.
* Consolidate `scalarCastHelper` + `boxValueOf` into a single
`scalarCastConversion` switch.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…riable type, EQ/NEQ numeric routing
* `parseStrictInteger` catches NumberFormatException from Long.parseLong and
rethrows as a parser-level IllegalArgumentException naming the slot, so
oversized rpm / index literals produce a clear error rather than leaking
a raw NumberFormatException at runtime.
* New `parseStrictInt` for the `[index]` slot range-checks against
Integer.MIN/MAX_VALUE before narrowing — `[3000000000]` now fails fast
instead of silently wrapping to a negative index.
* NumericLiteral.parse rejects bare integer literals that overflow Java's
long range (e.g. `999999999999999999999`) at parse time, with a clear
diagnostic suggesting fractional / D / F suffix forms for larger
magnitudes — instead of emitting an invalid `<huge>L` token that
Javassist fails to compile with an opaque error.
* generateBinaryExpression's String-concat end state now records
lastResolvedType=String.class and clears lastRawChain. Previously a
stale numeric type from an inner operand (e.g. the Integer cast in
`def msg = "count=" + (tag("n") as Integer)`) would leak into
generateDefStatement's local-variable declaration, producing an
Integer-typed variable that fails compilation when assigned a String.
* generateCondition's EQ/NEQ branch now routes through numeric
comparison when BOTH sides resolve to a numeric type — not just when
the RHS is a number literal. `tag("a") as Integer == tag("b") as Integer`
compares ints; `parsed.x as String != ""` keeps using Objects.equals
(fixes the network-profiling-slow-trace regression an earlier draft
introduced).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… suffixed-long range, method args
* generateBinaryExpression's string-concat path coerces with leading `""`
whenever neither side is statically a String at the transition point —
not just when the accumulator is non-numeric. `1 + parsed.field` (int +
Object) was emitting raw Java `+` which is a compile error;
now emits `"" + 1 + h.mapVal("field")` instead.
* NumericLiteral.parse validates L-suffixed bodies via Long.parseLong,
so `99999999999999999999L` raises a parser-level error instead of
slipping through to Javassist as an invalid Java token.
* generateMethodArgs handles arbitrary value-access nodes — binary
expressions (`foo(1 + 2)`), tag/parsed/log refs, paren-cast operands,
and chained accesses — by routing through generateValueAccess. For
numeric primitive results it uses the raw chain form (`(1 + 2)`)
rather than the boxed wrapper, so primitive-arg overloads like
`String.substring(int)` resolve correctly under Javassist (which does
not auto-unbox during method resolution). Number-literal args also
flow through NumericLiteral.parse for range validation.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Code consolidation:
* `inferComparisonOperandType` now delegates to `numericTypeOrNull` —
same shape, just a {@code LONG} fallback for non-numeric inputs.
* `inferRightHandType` reuses `numericTypeOrNull` for value-access RHS
type extraction.
* Four parallel `ExprType` switches (`javaPrimitiveName`,
`javaWrapperName`, `primitiveClass`, `wrapAsPrimitive` body) collapsed
into enum metadata fields on `ExprType`.
* Two near-identical `Class → ExprType` ladders in `inferType`
(def-variable resolution + proto-chain reflection) replaced by a
single `exprTypeFromClass` helper. `primitiveToExprType` removed —
call sites use the unified helper.
* `appendOperandRaw` numeric branch reuses the raw primitive chain when
available, avoiding `h.toX(boxed-null-safe)` which silently turns a
missing safe-nav chain into 0.
* `BinaryOp` carries its `symbol` field directly — drops the parallel
`opSymbol` switch.
* LHS rendering in `generateNumericComparison` now uses the same
`renderAsPrimitive` helper as the RHS path.
* `extractCastType` accepts a {@code null} context, eliminating the
`xx != null ? extractCastType(xx) : null` ternary at 13 callsites.
* `visitParserBlock` delegates JSON/YAML `abortOnFailure` extraction to
a single helper instead of inline duplicates.
* `visitFunctionArgs` uses a `literalArg` factory for the four
literal-shaped value-access constructions.
Bug fixes (Copilot review #5):
* Narrowing cast on an already-primitive operand preserves the inner
null-guard captured by `generateExtraLogAccess` (typed-proto safe-nav)
instead of clearing it via `recordPrimitiveResult` — a comparison
like `parsed?.x?.y as Long < 1` now still short-circuits to false on
a missing chain instead of NPE.
* `generateMethodArgs` honours the per-argument `castType` —
`substring(tag("n") as Integer)` now emits `h.toInt(...)` so primitive
overloads resolve under Javassist (cast was silently dropped before).
* Numeric arithmetic on safe-nav typed-proto primitives uses the raw
primitive chain instead of `h.toX(boxed-null-safe-expr)`. The old
form silently returned 0 for a missing chain, so
`parsed?.x?.y + 1` evaluated to 1 even when the chain was absent.
Behaviour now matches Java/Groovy: NPE on null + int rather than a
silent zero.
Tests:
* New UTs for the three silent-bug fixes above.
* Removed three UTs that just verified our parser-level checks for
oversized {@code long}/{@code L}-suffixed literals — those checks
themselves were dropped because Javassist already catches the same
invalid Java with a clear diagnostic referencing the user's literal.
Generated code:
* Skip the `h.toStr(...)` wrapper when the value is already a Java
String literal — `tag 'k': "v"` now compiles to
`_o.addTag("k", "v")` instead of `_o.addTag("k", h.toStr("v"))`.
The wrapper was a no-op since `h.toStr(String)` returns its argument
unchanged.
Implementation:
* `generateCastedValueAccess` collapsed from a 6-branch if/else chain
into a single helper-driven emission via `ExprType.runtimeHelper`
and `ExprType.primitiveClass`.
* `generateStringValueAccess` collapsed in the same way — numeric /
Boolean casts emit `String.valueOf(<helper>(...))`, the rest fall
through to the (now optimised) `h.toStr` path.
* New `isPlainStringLiteral` predicate centralises the
literal-detection logic shared between `generateCastedValueAccess`,
`generateStringValueAccess`, and `generateValueAccessObj`.
LAL arithmetic operators (+, -, *, /) with JLS-style type promotion
Builds on #13857. Generalises the LAL value-access expression grammar so all
four arithmetic operators (
+,-,*,/) work on numeric operands withproper precedence (
*//bind tighter than+/-), and the compileremits Java arithmetic in the user-declared primitive type instead of forcing
all integer arithmetic through
long.Operand types are inferred from explicit casts (
as Integer/as Long/as Float/as Double), typed proto fields, or numeric-literal shape. JLSbinary numeric promotion picks the wider primitive when sides differ.
(x as Integer) + (y as Integer)→int + int(waslong + long).(x as Integer) + (y as Long)→(long) ... + ...widened tolong.(x as Double) - 1→double - (double) 1.+with any String operand falls back to string concatenation;-/*//against non-numeric operands produces a compile-time error.Numeric literals now support Java-style suffixes —
1000Lfor long,1.5ffor float,1e3for double — withLvalid only on integerliterals (no decimal, no exponent), matching Java. The existing
typeCastclause accepts
DoubleandFloatin addition toString/Long/Integer/Boolean, including indefvariable declarations.Numeric comparisons now honour declared casts on both sides — previously the
compiler routed everything through
h.toLong().tag(\"a\") as Double < 1.5compiles to
h.toDouble(...) < 1.5;tag(\"a\") as Integer < 1.5compilesto
(double) h.toInt(...) < 1.5. Typed proto chains(
parsed?.x.y as Integer) keep their direct unboxed comparison path.