Skip to content

LAL: unify arithmetic operators (+ - * /) with JLS-style type promotion#13858

Merged
wu-sheng merged 7 commits intomasterfrom
feature/lal-arithmetic-ops
May 4, 2026
Merged

LAL: unify arithmetic operators (+ - * /) with JLS-style type promotion#13858
wu-sheng merged 7 commits intomasterfrom
feature/lal-arithmetic-ops

Conversation

@wu-sheng
Copy link
Copy Markdown
Member

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 with
proper precedence (* / / bind tighter than + / -), and the compiler
emits 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. JLS
binary numeric promotion picks the wider primitive when sides differ.

  • (x as Integer) + (y as Integer)int + int (was long + long).
  • (x as Integer) + (y as Long)(long) ... + ... widened to long.
  • (x as Double) - 1double - (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 — 1000L for long,
1.5f for float, 1e3 for double — with L valid only on integer
literals (no decimal, no exponent), matching Java. The existing typeCast
clause accepts Double and Float in addition to String / Long /
Integer / Boolean, including in def variable declarations.

Numeric comparisons now honour declared casts on both sides — previously the
compiler routed 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.

  • Add a unit test to verify that the fix works.
  • Explain briefly why the bug exists and how to fix it.
  • Update the `CHANGES` log.

…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.
@wu-sheng wu-sheng added backend OAP backend related. enhancement Enhancement on performance or codes logging Log collecting and analysis labels Apr 30, 2026
@wu-sheng wu-sheng added this to the 10.5.0 milestone Apr 30, 2026
@wu-sheng wu-sheng requested a review from Copilot May 3, 2026 12:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/Double casts, 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.

Comment thread docs/en/changes/changes.md Outdated
…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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

wu-sheng added 2 commits May 3, 2026 23:20
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`.
@wu-sheng wu-sheng merged commit 67160de into master May 4, 2026
860 of 871 checks passed
@wu-sheng wu-sheng deleted the feature/lal-arithmetic-ops branch May 4, 2026 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend OAP backend related. enhancement Enhancement on performance or codes logging Log collecting and analysis

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants