This is an automated email from the ASF dual-hosted git repository.

wusheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-graalvm-distro.git

commit dc6f5e1833cd87f58fdc23370edf84b53358e850
Author: Wu Sheng <[email protected]>
AuthorDate: Sat Feb 21 19:56:50 2026 +0800

    Runtime wiring: replace Groovy with pure Java MalExpression/MalFilter at 
runtime
    
    Wire transpiled MAL classes into runtime classpath:
    - Expression.java: wraps MalExpression instead of Groovy DelegatingScript
    - DSL.java: loads from mal-expressions.txt manifest with MalExpression 
classes
    - FilterExpression.java: loads from mal-filter-expressions.properties with 
MalFilter
    - Add Expression to shade excludes so same-FQCN replacement takes effect
    - Add InstanceEntityDescription same-FQCN replacement
    - Add native-image profile and Makefile targets (native-image, trace-agent)
    - Update MAL/LAL immigration docs with Phase 3 transpiler plans
---
 LAL-IMMIGRATION.md                                 |  81 ++++-
 MAL-IMMIGRATION.md                                 | 368 ++++++++++++++++++++-
 Makefile                                           |  14 +-
 build-tools/precompiler/MAL-TRANSPILER-DESIGN.md   | 219 ++++++++++++
 oap-graalvm-native/pom.xml                         |  37 +++
 .../meter-analyzer-for-graalvm/pom.xml             |   2 +
 .../skywalking/oap/meter/analyzer/dsl/DSL.java     |  44 ++-
 .../InstanceEntityDescription.java                 |  51 +++
 .../oap/meter/analyzer/dsl/Expression.java         |  90 +++++
 .../oap/meter/analyzer/dsl/FilterExpression.java   |  38 +--
 pom.xml                                            |   1 +
 11 files changed, 896 insertions(+), 49 deletions(-)

diff --git a/LAL-IMMIGRATION.md b/LAL-IMMIGRATION.md
index e6c18cc..6b54950 100644
--- a/LAL-IMMIGRATION.md
+++ b/LAL-IMMIGRATION.md
@@ -1,4 +1,4 @@
-# Phase 2: LAL Build-Time Pre-Compilation — COMPLETE
+# LAL Immigration: Build-Time Pre-Compilation + Groovy Elimination
 
 ## Context
 
@@ -218,3 +218,82 @@ JAVA_HOME=/Users/wusheng/.sdkman/candidates/java/25-graal 
make build-distro
 ```
 
 Expected: 19 comparison tests across 5 test classes, all passing.
+
+---
+
+# Phase 3: Eliminate Groovy — Pure Java LAL Transpiler (Deferred)
+
+## Context
+
+LAL uses `@CompileStatic` Groovy, which generates standard Java bytecode 
without
+dynamic MOP. However, the Groovy runtime library is still required at runtime 
for
+base classes (`DelegatingScript`, `Closure`, `Binding`, `Script`). Since MAL's
+Phase 3 eliminates Groovy from runtime entirely, LAL must follow the same 
approach
+to fully remove the Groovy dependency.
+
+**Priority**: LAL transpilation is deferred until after MAL transpilation is 
complete
+and validated. There are only 10 LAL scripts (6 unique after SHA-256 dedup), 
compared
+to 1250+ MAL expressions. LAL can be tackled separately.
+
+---
+
+## Approach
+
+The same transpiler pattern used for MAL applies to LAL, but LAL scripts are 
more
+complex — they are full programs with nested spec calls rather than method 
chains:
+
+```groovy
+filter {
+    if (tag("LOG_KIND") == "NET_PROFILING_SAMPLED_TRACE") {
+        json {
+            tag("address", parsed.address)
+        }
+        sampledTrace {
+            // ...
+        }
+    } else {
+        abort {}
+    }
+}
+```
+
+### Option A (Recommended): Transpile LAL to Java
+
+Generate pure Java classes that implement the same logic:
+
+```java
+public class LalScript_envoy_als implements LalExpression {
+    @Override
+    public void run(FilterSpec filterSpec, Binding binding) {
+        filterSpec.filter(() -> {
+            if 
("NET_PROFILING_SAMPLED_TRACE".equals(binding.getTag("LOG_KIND"))) {
+                filterSpec.json(() -> {
+                    binding.tag("address", binding.getParsed().get("address"));
+                });
+                filterSpec.sampledTrace(() -> { ... });
+            } else {
+                filterSpec.abort();
+            }
+        });
+    }
+}
+```
+
+### Option B: Minimal Groovy Stubs
+
+Keep LAL as `@CompileStatic` pre-compiled bytecode but provide minimal stubs 
for
+the few Groovy base classes needed (`DelegatingScript`, `Closure`, `Binding`).
+This avoids the full Groovy runtime while keeping the pre-compiled bytecode 
working.
+
+---
+
+## Implementation (when ready)
+
+1. Create `LalExpression` interface
+2. Create `LalToJavaTranspiler` (similar to `MalToJavaTranspiler`)
+3. Handle LAL-specific patterns: `filter {}`, `json {}`, `text { regexp }`,
+   `extractor {}`, `sink {}`, `sampledTrace {}`, `slowSql {}`, `abort {}`
+4. Replace LAL `DSL.java` to load `LalExpression` instead of `DelegatingScript`
+5. Update precompiler `compileLAL()` to use transpiler
+6. Run comparison tests (19 tests across 5 classes)
+7. Remove Groovy from runtime classpath
diff --git a/MAL-IMMIGRATION.md b/MAL-IMMIGRATION.md
index 499df5a..99ae638 100644
--- a/MAL-IMMIGRATION.md
+++ b/MAL-IMMIGRATION.md
@@ -1,4 +1,4 @@
-# Phase 2: MAL/LAL Build-Time Pre-Compilation — COMPLETE
+# MAL Immigration: Build-Time Pre-Compilation + Groovy Elimination
 
 ## Context
 
@@ -456,3 +456,369 @@ cat 
build-tools/oal-exporter/target/generated-oal-classes/META-INF/annotation-sc
 # 6. Verify tests pass
 make build-distro  # runs MALCompilerTest + MALPrecompiledRegistrationTest
 ```
+
+---
+
+# Phase 3: Eliminate Groovy — Pure Java MAL Transpiler
+
+## Why: Groovy + GraalVM Native Image is Incompatible
+
+The first native-image build (`make native-image`) failed with:
+
+```
+Error: Could not find target method:
+  protected static void com.oracle.svm.polyglot.groovy
+    
.Target_org_codehaus_groovy_vmplugin_v7_IndyInterface_invalidateSwitchPoints
+    .invalidateSwitchPoints()
+```
+
+**Root cause**: GraalVM's built-in `GroovyIndyInterfaceFeature` (in 
`library-support.jar`)
+targets `IndyInterface.invalidateSwitchPoints()` — a method removed in Groovy 
5.0.3.
+The Groovy 5.x runtime still uses `org.codehaus.groovy` packages and 
`invokedynamic`
+bootstrapping, but the internal API changed. GraalVM's substitution layer 
cannot find
+the target method.
+
+**Why fixing Groovy is not worth it**: Even if the substitution were patched, 
Groovy's
+dynamic runtime (ExpandoMetaClass, MOP, IndyInterface, MetaClassRegistry) 
requires
+extensive GraalVM reflection/substitution configuration. The MAL pre-compiled 
scripts
+use `invokedynamic` + Groovy MOP for method resolution. Making this work 
reliably in
+native-image would be fragile and complex.
+
+**Solution**: Eliminate Groovy from runtime entirely. Generate pure Java code 
at build
+time that implements the same MAL expression logic. The existing 1303 UTs 
validate
+that the generated Java code produces identical results to the Groovy-compiled 
scripts.
+
+---
+
+## Approach: MAL-to-Java Transpiler
+
+**Key insight**: MAL expressions are method chains on `SampleFamily` with a 
well-defined
+API. The precompiler already parses all 1250+ MAL rules. Instead of compiling 
them to
+Groovy bytecode (which needs Groovy runtime), we parse the Groovy AST at build 
time and
+generate equivalent Java source code. Zero Groovy at runtime.
+
+### What Changes
+
+| Aspect | Phase 2 (Groovy pre-compilation) | Phase 3 (Java transpiler) |
+|--------|----------------------------------|--------------------------|
+| Build output | Groovy `.class` bytecode | Pure Java `.class` files |
+| Runtime dependency | Groovy runtime (MOP, ExpandoMetaClass) | None |
+| Expression execution | `DelegatingScript.run()` + `ExpandoMetaClass` | 
`MalExpression.run(Map<String, SampleFamily>)` |
+| Closure parameters | `groovy.lang.Closure` | Java functional interfaces |
+| `propertyMissing()` | Groovy MOP resolves metric names | 
`samples.get("metricName")` |
+| `Number * SampleFamily` | `ExpandoMetaClass` on `Number` | 
`sf.multiply(number)` (transpiler swaps operands) |
+| Filter expressions | Groovy `Script.run()` → `Closure<Boolean>` | 
`SampleFilter.test(Map<String, String>)` |
+
+### What Stays the Same
+
+- **MeterSystem Javassist generation**: Pre-compiled at build time (Phase 2), 
unchanged
+- **Config data serialization**: JSON manifests for rule configs, unchanged
+- **Manifest-based loading**: Same pattern, new manifest format for Java 
expressions
+- **MetricConvert pipeline**: Still runs at build time for Javassist class 
generation
+
+---
+
+## Step 1: Java Functional Interfaces for Closure Replacements
+
+5 `SampleFamily` methods accept `groovy.lang.Closure` parameters. Replace with 
Java
+functional interfaces:
+
+| Method | Closure Type | Java Interface |
+|--------|-------------|----------------|
+| `tag(Closure<?> cl)` | `Map<String,String> → Map<String,String>` | 
`TagFunction extends Function<Map, Map>` |
+| `filter(Closure<Boolean> f)` | `Map<String,String> → Boolean` | 
`SampleFilter extends Predicate<Map>` |
+| `forEach(List, Closure each)` | `(String, Map) → void` | `ForEachFunction 
extends BiConsumer<String, Map>` |
+| `decorate(Closure c)` | `MeterEntity → void` | `DecorateFunction extends 
Consumer<MeterEntity>` |
+| `instance(..., Closure extractor)` | `Map<String,String> → 
Map<String,String>` | `PropertiesExtractor extends Function<Map, Map>` |
+
+**Files created**:
+- `oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/MalExpression.java`
+- 
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/SampleFamilyFunctions.java`
+
+### MalExpression Interface
+
+```java
+public interface MalExpression {
+    SampleFamily run(Map<String, SampleFamily> samples);
+}
+```
+
+Each generated MAL expression class implements this interface. The `samples` 
map
+replaces Groovy's `propertyMissing()` delegate — metric names are resolved via
+`samples.get("metricName")`.
+
+### MalFilter Interface
+
+```java
+public interface MalFilter {
+    boolean test(Map<String, String> tags);
+}
+```
+
+Each generated filter expression class implements this interface.
+
+---
+
+## Step 2: Same-FQCN SampleFamily Replacement
+
+**File**: 
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/SampleFamily.java`
+
+Copy upstream `SampleFamily.java` and change ONLY the 5 Closure-based method 
signatures:
+
+```java
+// Before (upstream)
+public SampleFamily tag(Closure<?> cl) { ... cl.rehydrate(...).call(arg) ... }
+public SampleFamily filter(Closure<Boolean> filter) { ... 
filter.call(it.labels) ... }
+public SampleFamily forEach(List<String> array, Closure<Void> each) { ... 
each.call(element, labels) ... }
+public SampleFamily decorate(Closure<Void> c) { ... c.call(meterEntity) ... }
+public SampleFamily instance(..., Closure<Map<String,String>> 
propertiesExtractor) { ... }
+
+// After (replacement)
+public SampleFamily tag(TagFunction fn) { ... fn.apply(arg) ... }
+public SampleFamily filter(SampleFilter f) { ... f.test(it.labels) ... }
+public SampleFamily forEach(List<String> array, ForEachFunction each) { ... 
each.accept(element, labels) ... }
+public SampleFamily decorate(DecorateFunction fn) { ... fn.accept(meterEntity) 
... }
+public SampleFamily instance(..., PropertiesExtractor extractor) { ... }
+```
+
+All other methods (tagEqual, sum, avg, histogram, service, endpoint, etc.) 
remain
+identical — they don't use Groovy types.
+
+**Shade config**: Add `SampleFamily.class` and `SampleFamily$*.class` to shade 
excludes.
+
+---
+
+## Step 3: MAL-to-Java Transpiler
+
+**File**: `build-tools/precompiler/.../MalToJavaTranspiler.java`
+
+Parses MAL expression strings using Groovy's parser (available at build time, 
not needed
+at runtime) and generates pure Java source code.
+
+### Input → Output Example
+
+**Input** (MAL expression string from apisix.yaml):
+```
+apisix_http_status.tagNotEqual('route','').tag({tags->tags.route='route/'+tags['route']})
+  
.sum(['code','service_name','route','node']).rate('PT1M').endpoint(['service_name'],['route'],
 Layer.APISIX)
+```
+
+**Output** (generated Java class):
+```java
+package org.apache.skywalking.oap.server.core.source.oal.rt.mal;
+
+import java.util.*;
+import org.apache.skywalking.oap.meter.analyzer.dsl.*;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+
+public class MalExpr_meter_apisix_endpoint_http_status implements 
MalExpression {
+    @Override
+    public SampleFamily run(Map<String, SampleFamily> samples) {
+        SampleFamily v0 = samples.get("apisix_http_status");
+        if (v0 == null) return SampleFamily.EMPTY;
+        return v0
+            .tagNotEqual("route", "")
+            .tag(tags -> { tags.put("route", "route/" + tags.get("route")); 
return tags; })
+            .sum(List.of("code", "service_name", "route", "node"))
+            .rate("PT1M")
+            .endpoint(List.of("service_name"), List.of("route"), Layer.APISIX);
+    }
+}
+```
+
+### AST Node → Java Output Mapping
+
+| Groovy AST Node | Java Output |
+|-----------------|-------------|
+| Bare property access (`metric_name`) | `samples.get("metric_name")` |
+| Method call on SampleFamily | Direct method call (same name) |
+| `Closure` in `.tag()` | Lambda: `tags -> { tags.put(...); return tags; }` |
+| `Closure` in `.filter()` | Lambda: `labels -> booleanExpr` |
+| `Closure` in `.forEach()` | Lambda: `(element, labels) -> { ... }` |
+| `tags.name` inside closure | `tags.get("name")` |
+| `tags.name = val` inside closure | `tags.put("name", val)` |
+| `tags['key']` inside closure | `tags.get("key")` |
+| Binary `SF + SF` | `left.plus(right)` |
+| Binary `SF - SF` | `left.minus(right)` |
+| Binary `SF * SF` | `left.multiply(right)` |
+| Binary `SF / SF` | `left.div(right)` |
+| Binary `Number * SF` | `sf.multiply(number)` (swap operands) |
+| Binary `Number + SF` | `sf.plus(number)` (swap operands) |
+| Binary `Number - SF` | `sf.minus(number).negative()` (swap operands) |
+| Binary `Number / SF` | `sf.newValue(v -> number / v)` (swap operands) |
+| Unary `-SF` | `sf.negative()` |
+| `['a','b','c']` (list literal) | `List.of("a", "b", "c")` |
+| `[50,75,90,95,99]` (int list) | `List.of(50, 75, 90, 95, 99)` |
+| String literal `'foo'` | `"foo"` |
+| Enum constant `Layer.APISIX` | `Layer.APISIX` |
+| Enum constant `DetectPoint.SERVER` | `DetectPoint.SERVER` |
+| `DownsamplingType.LATEST` / `LATEST` | `DownsamplingType.LATEST` |
+| `time()` function | `java.time.Instant.now().getEpochSecond()` |
+| `a?.b` (safe navigation) | `a != null ? a.b : null` |
+| `a ?: b` (elvis) | `a != null ? a : b` |
+
+### Filter Expression Transpilation
+
+Filter expressions are simpler — they are boolean closures:
+
+**Input**: `{ tags -> tags.job_name == 'apisix-monitoring' }`
+
+**Output**:
+```java
+public class MalFilter_apisix implements MalFilter {
+    @Override
+    public boolean test(Map<String, String> tags) {
+        return "apisix-monitoring".equals(tags.get("job_name"));
+    }
+}
+```
+
+---
+
+## Step 4: Same-FQCN Expression.java Replacement
+
+**File**: 
`oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/Expression.java`
+
+Simplified — no DelegatingScript, no ExpandoMetaClass, no ExpressionDelegate:
+
+```java
+public class Expression {
+    private final String metricName;
+    private final String literal;
+    private final MalExpression expression;  // Pure Java
+
+    public Result run(Map<String, SampleFamily> sampleFamilies) {
+        SampleFamily sf = expression.run(sampleFamilies);
+        if (sf == SampleFamily.EMPTY) return Result.fail("EMPTY");
+        return Result.success(sf);
+    }
+
+    // parse() still provides ExpressionParsingContext for static analysis
+    // (scopeType, functionName, etc.) — this is unchanged
+}
+```
+
+---
+
+## Step 5: Update DSL.java and FilterExpression.java
+
+### DSL.java (already exists, update)
+
+Change from loading `DelegatingScript` to loading `MalExpression`:
+
+```java
+public static Expression parse(String metricName, String expression) {
+    MalExpression malExpr = loadFromManifest(metricName);  // Class.forName + 
newInstance
+    return new Expression(metricName, expression, malExpr);
+}
+```
+
+Manifest: `META-INF/mal-expressions.txt` (replaces `mal-groovy-scripts.txt`):
+```
+meter_apisix_endpoint_http_status=org.apache.skywalking.oap.server.core.source.oal.rt.mal.MalExpr_meter_apisix_endpoint_http_status
+```
+
+### FilterExpression.java (already exists, update)
+
+Change from loading Groovy `Script` → `Closure<Boolean>` to loading 
`MalFilter`:
+
+```java
+public class FilterExpression {
+    private final MalFilter filter;
+
+    public FilterExpression(String literal) {
+        this.filter = loadFromManifest(literal);  // Class.forName + 
newInstance
+    }
+
+    public Map<String, SampleFamily> filter(Map<String, SampleFamily> 
sampleFamilies) {
+        // Apply filter.test(labels) to each sample family
+    }
+}
+```
+
+---
+
+## Step 6: Update Precompiler
+
+Replace `compileMAL()` in `Precompiler.java` to use the transpiler instead of 
Groovy
+compilation:
+
+```java
+private static void compileMAL(String outputDir, ...) {
+    MalToJavaTranspiler transpiler = new MalToJavaTranspiler(outputDir);
+
+    for (Rule rule : allRules) {
+        for (MetricsRule mr : rule.getMetricsRules()) {
+            String metricName = formatMetricName(rule, mr.getName());
+            String expression = formatExp(rule.getExpPrefix(), 
rule.getExpSuffix(), mr.getExp());
+            transpiler.transpile(metricName, expression);
+        }
+        if (rule.getFilter() != null) {
+            transpiler.transpileFilter(rule.getName(), rule.getFilter());
+        }
+    }
+
+    transpiler.writeManifest();   // mal-expressions.txt
+    transpiler.compileAll();      // javac on generated .java files
+}
+```
+
+The Javassist meter class generation is still run via `MetricConvert` pipeline 
(unchanged
+from Phase 2). Only Groovy compilation is replaced by transpilation.
+
+---
+
+## Same-FQCN Replacements (Phase 3 — additions/updates)
+
+| Upstream Class | Replacement Location | Phase 3 Change |
+|---|---|---|
+| `SampleFamily` | `meter-analyzer-for-graalvm/` | **New.** Closure parameters 
→ functional interfaces |
+| `Expression` | `meter-analyzer-for-graalvm/` | **New.** No Groovy: uses 
`MalExpression` instead of `DelegatingScript` |
+| `DSL` (MAL) | `meter-analyzer-for-graalvm/` | **Updated.** Loads 
`MalExpression` instead of Groovy `DelegatingScript` |
+| `FilterExpression` | `meter-analyzer-for-graalvm/` | **Updated.** Loads 
`MalFilter` instead of Groovy `Closure<Boolean>` |
+
+---
+
+## Files Created (Phase 3)
+
+| File | Purpose |
+|------|---------|
+| `.../dsl/MalExpression.java` | Interface for generated MAL expression 
classes |
+| `.../dsl/MalFilter.java` | Interface for generated MAL filter classes |
+| `.../dsl/SampleFamilyFunctions.java` | Functional interfaces: TagFunction, 
SampleFilter, ForEachFunction, DecorateFunction, PropertiesExtractor |
+| `.../meter-analyzer-for-graalvm/SampleFamily.java` | Same-FQCN: Closure → 
functional interfaces |
+| `.../meter-analyzer-for-graalvm/Expression.java` | Same-FQCN: no Groovy, 
uses MalExpression |
+| `.../precompiler/MalToJavaTranspiler.java` | AST-walking transpiler: Groovy 
AST → Java source |
+| Generated `MalExpr_*.java` files | ~1250 pure Java expression classes |
+| Generated `MalFilter_*.java` files | ~29 pure Java filter classes |
+
+## Files Modified (Phase 3)
+
+| File | Change |
+|------|--------|
+| `Precompiler.java` | `compileMAL()` uses transpiler instead of Groovy 
compilation |
+| `meter-analyzer-for-graalvm/pom.xml` | Add SampleFamily, Expression to shade 
excludes |
+| `meter-analyzer-for-graalvm/DSL.java` | Load MalExpression instead of 
DelegatingScript |
+| `meter-analyzer-for-graalvm/FilterExpression.java` | Load MalFilter instead 
of Groovy Closure |
+
+---
+
+## Verification (Phase 3)
+
+```bash
+# 1. Rebuild with transpiler
+make compile
+
+# 2. Run ALL existing tests (1303 UTs validate identical MAL behavior)
+make test
+
+# 3. Boot JVM distro
+make shutdown && make boot
+
+# 4. Verify no Groovy at runtime (after LAL is also transpiled)
+jar tf 
oap-graalvm-server/target/oap-graalvm-jvm-distro/oap-graalvm-jvm-distro/libs/*.jar
 | grep groovy
+# Should find NOTHING
+
+# 5. Attempt native-image build (no more Groovy substitution error)
+make native-image
+```
diff --git a/Makefile b/Makefile
index 27430d3..1ef37aa 100644
--- a/Makefile
+++ b/Makefile
@@ -19,7 +19,7 @@ SW_VERSION := $(shell grep '<revision>' skywalking/pom.xml | 
head -1 | sed 's/.*
 MVN := ./mvnw
 MVN_ARGS := -Dskywalking.version=$(SW_VERSION)
 
-.PHONY: all clean build init-submodules build-skywalking build-distro compile 
test javadoc dist info docker-up docker-down boot shutdown
+.PHONY: all clean build init-submodules build-skywalking build-distro compile 
test javadoc dist info docker-up docker-down boot shutdown native-image 
trace-agent
 
 all: build
 
@@ -85,6 +85,18 @@ docker-down:
 shutdown:
        
@oap-graalvm-server/target/oap-graalvm-jvm-distro/oap-graalvm-jvm-distro/bin/oapServiceStop.sh
 2>/dev/null || true
 
+# Build native image (requires GraalVM JDK)
+native-image: compile
+       $(MVN) package -pl oap-graalvm-native -Pnative -DskipTests $(MVN_ARGS)
+
+# Run tracing agent to capture supplementary native-image metadata
+# Merges with pre-generated reflect-config.json from the precompiler
+trace-agent: build-distro docker-up
+       SW_STORAGE_BANYANDB_TARGETS=localhost:17912 \
+       SW_CLUSTER=standalone \
+       JAVA_OPTS="-Xms256M -Xmx4096M 
-agentlib:native-image-agent=config-merge-dir=oap-graalvm-native/src/main/resources/META-INF/native-image/org.apache.skywalking/oap-graalvm-native"
 \
+         
oap-graalvm-server/target/oap-graalvm-jvm-distro/oap-graalvm-jvm-distro/bin/oapService.sh
+
 # Build distro and boot OAP with BanyanDB
 boot: build-distro docker-up
        SW_STORAGE_BANYANDB_TARGETS=localhost:17912 \
diff --git a/build-tools/precompiler/MAL-TRANSPILER-DESIGN.md 
b/build-tools/precompiler/MAL-TRANSPILER-DESIGN.md
new file mode 100644
index 0000000..4ace716
--- /dev/null
+++ b/build-tools/precompiler/MAL-TRANSPILER-DESIGN.md
@@ -0,0 +1,219 @@
+# Phase 3: MAL-to-Java Transpiler Design
+
+## Context
+
+Phase 2 (Groovy pre-compilation) is complete. Phase 3 Step 1 (functional 
interfaces) and Step 2 (SampleFamily replacement) are done. This document 
covers Steps 3-6: the transpiler, Expression.java replacement, 
DSL/FilterExpression updates, and Precompiler wiring.
+
+**Goal**: Eliminate Groovy from runtime. Generate pure Java classes at build 
time implementing `MalExpression` and `MalFilter`. Groovy is still used at 
build time (for AST parsing and the MetricConvert/Javassist pipeline) but NOT 
at runtime.
+
+---
+
+## Architecture
+
+```
+MalToJavaTranspiler
+├── transpile(metricName, expression)       → generates MalExpr_<name>.java
+├── transpileFilter(filterLiteral)          → generates MalFilter_<N>.java
+├── writeManifests()                        → mal-expressions.txt + 
mal-filter-expressions.properties
+├── compileAll()                            → javax.tools.JavaCompiler on 
generated .java files
+│
+├── (internal) parseToAST(expression)       → Groovy CompilationUnit → 
ModuleNode
+├── (internal) visitExpression(ASTNode)     → String (Java code fragment)
+├── (internal) visitMethodCall(node)        → method chain translation
+├── (internal) visitBinary(node)            → arithmetic with operand-swap 
logic
+├── (internal) visitClosure(node, type)     → Java lambda
+├── (internal) visitProperty(node)          → tags.get("key") or 
samples.get("name")
+└── (internal) visitConstant(node)          → literals, enums
+```
+
+---
+
+## Generated Class Template (Expressions)
+
+```java
+package org.apache.skywalking.oap.server.core.source.oal.rt.mal;
+import java.util.*;
+import org.apache.skywalking.oap.meter.analyzer.dsl.*;
+import org.apache.skywalking.oap.meter.analyzer.dsl.SampleFamilyFunctions.*;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.source.DetectPoint;
+import org.apache.skywalking.oap.meter.analyzer.dsl.tagOpt.K8sRetagType;
+
+public class MalExpr_meter_xxx implements MalExpression {
+    @Override
+    public SampleFamily run(Map<String, SampleFamily> samples) {
+        ExpressionParsingContext.get().ifPresent(ctx -> {
+            ctx.samples.add("metric_name");
+        });
+        SampleFamily v0 = samples.getOrDefault("metric_name", 
SampleFamily.EMPTY);
+        return v0.sum(List.of("a", "b")).service(List.of("svc"), 
Layer.GENERAL);
+    }
+}
+```
+
+**ExpressionParsingContext tracking**: Generated `run()` adds all referenced 
sample names to `ExpressionParsingContext.samples`. Needed for 
`Expression.parse()` which calls `run(ImmutableMap.of())`. SampleFamily methods 
already update scope/function/labels/histogram/downsampling in the parsing 
context.
+
+**metricName on RunningContext**: `Expression.run()` sets 
`sf.context.setMetricName(metricName)` on all input SampleFamily objects before 
calling `MalExpression.run()`.
+
+---
+
+## AST Node → Java Mapping
+
+### Core Nodes
+
+| Groovy AST | Java Output | Example |
+|---|---|---|
+| `VariableExpression` (sample name) | `samples.getOrDefault("name", 
SampleFamily.EMPTY)` | `metric_name` → local var |
+| `VariableExpression` (DownsamplingType) | `DownsamplingType.X` | `SUM` → 
`DownsamplingType.SUM` |
+| `MethodCallExpression` | Direct method call | `.sum(['a'])` → 
`.sum(List.of("a"))` |
+| `ConstantExpression` (string) | `"string"` | `'PT1M'` → `"PT1M"` |
+| `ConstantExpression` (number) | number literal | `100` → `100` |
+| `ListExpression` (strings) | `List.of(...)` | `['a','b']` → `List.of("a", 
"b")` |
+| `ListExpression` (integers) | `List.of(...)` | `[50,75,90]` → `List.of(50, 
75, 90)` |
+| `PropertyExpression` (enum) | Qualified enum | `Layer.GENERAL` → 
`Layer.GENERAL` |
+
+### Binary Operations
+
+| Groovy | Java | Notes |
+|---|---|---|
+| `SF + SF` | `left.plus(right)` | |
+| `SF - SF` | `left.minus(right)` | |
+| `SF * SF` | `left.multiply(right)` | |
+| `SF / SF` | `left.div(right)` | |
+| `SF + N` | `sf.plus(N)` | |
+| `SF - N` | `sf.minus(N)` | |
+| `SF * N` | `sf.multiply(N)` | |
+| `SF / N` | `sf.div(N)` | |
+| `N + SF` | `sf.plus(N)` | Swap operands |
+| `N - SF` | `sf.minus(N).negative()` | From ExpandoMetaClass |
+| `N * SF` | `sf.multiply(N)` | Swap operands |
+| `N / SF` | `sf.newValue(v -> N / v)` | From ExpandoMetaClass |
+
+### Closure Patterns
+
+#### `.tag(Closure)` → `TagFunction` lambda
+
+| Groovy inside closure | Java output |
+|---|---|
+| `tags.key` (read) | `tags.get("key")` |
+| `tags['key']` (subscript read) | `tags.get("key")` |
+| `tags[expr]` (dynamic subscript) | `tags.get(expr)` |
+| `tags.key = val` (write) | `tags.put("key", val)` |
+| `tags[expr] = val` (dynamic write) | `tags.put(expr, val)` |
+| `tags.remove('key')` | `tags.remove("key")` |
+| `tags.ApiId ? A : B` | `(tags.get("ApiId") != null && 
!tags.get("ApiId").isEmpty()) ? A : B` |
+| `tags['x']?.trim()` | `tags.get("x") != null ? tags.get("x").trim() : null` |
+| `expr ?: 'default'` | `(expr != null ? expr : "default")` |
+| `if (...) { ... }` | Same in Java |
+| `if (...) { ... } else { ... }` | Same in Java |
+
+#### File-level `filter:` → `MalFilter` class
+
+| Groovy | Java |
+|---|---|
+| `tags.key == 'val'` | `"val".equals(tags.get("key"))` |
+| `tags.key in ['a', 'b']` | `List.of("a", "b").contains(tags.get("key"))` |
+| `tags.key` (truthiness) | `tags.get("key") != null && 
!tags.get("key").isEmpty()` |
+| `!tags.key` (negated) | `tags.get("key") == null \|\| 
tags.get("key").isEmpty()` |
+| `{compound && (a \|\| b)}` | Unwrap block, translate body |
+
+#### `.forEach(List, Closure)` → `ForEachFunction` lambda
+
+| Groovy | Java |
+|---|---|
+| `tags[prefix + '_key']` | `tags.get(prefix + "_key")` |
+| `tags[prefix + '_key'] = val` | `tags.put(prefix + "_key", val)` |
+| `tags.service` | `tags.get("service")` |
+| `return` (early exit) | `return` (same in BiConsumer lambda) |
+| `ProcessRegistry.method(...)` | Same static call |
+
+#### `.instance(..., Closure)` propertiesExtractor → `PropertiesExtractor` 
lambda
+
+`{tags -> ['pod': tags.pod, 'namespace': tags.namespace]}` → `tags -> 
Map.of("pod", tags.get("pod"), "namespace", tags.get("namespace"))`
+
+---
+
+## Known DownsamplingType Constants
+
+Bare names in Groovy → qualified in Java:
+- `AVG` → `DownsamplingType.AVG`
+- `SUM` → `DownsamplingType.SUM`
+- `LATEST` → `DownsamplingType.LATEST`
+- `SUM_PER_MIN` → `DownsamplingType.SUM_PER_MIN`
+- `MAX` → `DownsamplingType.MAX`
+- `MIN` → `DownsamplingType.MIN`
+
+---
+
+## Expression.java Replacement
+
+Same FQCN as upstream. Uses `MalExpression` instead of `DelegatingScript`.
+
+No `empower()`, no `ExpandoMetaClass`, no `ExpressionDelegate`, no 
`ThreadLocal`.
+
+```java
+public class Expression {
+    private final String metricName;
+    private final String literal;
+    private final MalExpression expression;
+
+    public Expression(String metricName, String literal, MalExpression 
expression) { ... }
+
+    public ExpressionParsingContext parse() {
+        try (ExpressionParsingContext ctx = ExpressionParsingContext.create()) 
{
+            Result r = run(ImmutableMap.of());
+            if (!r.isSuccess() && r.isThrowable()) {
+                throw new ExpressionParsingException(...);
+            }
+            ctx.validate(literal);
+            return ctx;
+        }
+    }
+
+    public Result run(Map<String, SampleFamily> sampleFamilies) {
+        for (SampleFamily sf : sampleFamilies.values()) {
+            if (sf != null && sf != SampleFamily.EMPTY) {
+                sf.context.setMetricName(metricName);
+            }
+        }
+        try {
+            SampleFamily sf = expression.run(sampleFamilies);
+            if (sf == SampleFamily.EMPTY) { return Result.fail(...); }
+            return Result.success(sf);
+        } catch (Throwable t) {
+            return Result.fail(t);
+        }
+    }
+}
+```
+
+---
+
+## Precompiler Integration
+
+`compileMAL()` changes:
+1. Still runs MetricConvert for Javassist meter class generation (unchanged)
+2. Groovy scripts → temp dir (not exported)
+3. Transpiler generates Java expression .class files → output dir
+4. Manifests: `mal-expressions.txt` (replaces `mal-groovy-scripts.txt`), 
`mal-filter-expressions.properties` (replaces `mal-filter-scripts.properties`)
+
+**Combination pattern**: Transpiler tracks duplicate metric names, appends 
`_1`, `_2` suffixes.
+
+---
+
+## Files Summary
+
+### New Files
+| File | Purpose |
+|---|---|
+| `build-tools/precompiler/.../MalToJavaTranspiler.java` | AST-walking 
transpiler |
+| `oap-libs-for-graalvm/meter-analyzer-for-graalvm/.../dsl/Expression.java` | 
Same-FQCN: MalExpression instead of DelegatingScript |
+
+### Modified Files
+| File | Change |
+|---|---|
+| `.../meter-analyzer-for-graalvm/.../dsl/DSL.java` | Load MalExpression; 
manifest: mal-expressions.txt |
+| `.../meter-analyzer-for-graalvm/.../dsl/FilterExpression.java` | Load 
MalFilter; manifest: mal-filter-expressions.properties |
+| `.../meter-analyzer-for-graalvm/.../dsl/SampleFamily.java` | `newValue()` → 
`public` |
+| `.../meter-analyzer-for-graalvm/pom.xml` | Add Expression.class to shade 
excludes |
+| `build-tools/precompiler/.../Precompiler.java` | Add transpiler call in 
compileMAL() |
diff --git a/oap-graalvm-native/pom.xml b/oap-graalvm-native/pom.xml
index b91e86e..b666361 100644
--- a/oap-graalvm-native/pom.xml
+++ b/oap-graalvm-native/pom.xml
@@ -42,4 +42,41 @@
             <scope>provided</scope>
         </dependency>
     </dependencies>
+
+    <profiles>
+        <profile>
+            <id>native</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.graalvm.buildtools</groupId>
+                        <artifactId>native-maven-plugin</artifactId>
+                        <version>${graalvm.buildtools.version}</version>
+                        <extensions>true</extensions>
+                        <configuration>
+                            
<mainClass>org.apache.skywalking.oap.server.graalvm.GraalVMOAPServerStartUp</mainClass>
+                            <imageName>oap-server</imageName>
+                            <metadataRepository>
+                                <enabled>true</enabled>
+                            </metadataRepository>
+                            <buildArgs>
+                                <buildArg>--no-fallback</buildArg>
+                                <buildArg>--verbose</buildArg>
+                                
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
+                            </buildArgs>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>build-native</id>
+                                <goals>
+                                    <goal>compile-no-fork</goal>
+                                </goals>
+                                <phase>package</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
 </project>
\ No newline at end of file
diff --git a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml
index 2173682..162eb27 100644
--- a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml
+++ b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/pom.xml
@@ -67,6 +67,8 @@
                                 
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression$*.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/SampleFamily$*.class</exclude>
+                                
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/Expression.class</exclude>
+                                
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/Expression$*.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/ExpressionParsingContext$*.class</exclude>
                                 
<exclude>org/apache/skywalking/oap/meter/analyzer/dsl/EntityDescription/InstanceEntityDescription.class</exclude>
diff --git 
a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
index d766fcf..c728c4f 100644
--- 
a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
+++ 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/DSL.java
@@ -17,7 +17,6 @@
 
 package org.apache.skywalking.oap.meter.analyzer.dsl;
 
-import groovy.util.DelegatingScript;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -29,17 +28,13 @@ import java.util.concurrent.atomic.AtomicInteger;
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * GraalVM replacement for upstream MAL DSL.
- * Original: 
skywalking/oap-server/analyzer/meter-analyzer/src/main/java/.../dsl/DSL.java
- * Repackaged into meter-analyzer-for-graalvm via maven-shade-plugin (replaces 
original .class in shaded JAR).
- *
- * Change: Complete rewrite. Loads pre-compiled Groovy DelegatingScript 
classes from
- * META-INF/mal-groovy-scripts.txt manifest instead of GroovyShell runtime 
compilation.
- * Why: Groovy runtime compilation is incompatible with GraalVM native image.
+ * Same-FQCN replacement for upstream MAL DSL.
+ * Loads transpiled MalExpression classes from mal-expressions.txt manifest
+ * instead of Groovy DelegatingScript classes — no Groovy runtime needed.
  */
 @Slf4j
 public final class DSL {
-    private static final String MANIFEST_PATH = 
"META-INF/mal-groovy-scripts.txt";
+    private static final String MANIFEST_PATH = "META-INF/mal-expressions.txt";
     private static volatile Map<String, String> SCRIPT_MAP;
     private static final AtomicInteger LOADED_COUNT = new AtomicInteger();
 
@@ -54,22 +49,22 @@ public final class DSL {
         String className = scriptMap.get(metricName);
         if (className == null) {
             throw new IllegalStateException(
-                "Pre-compiled MAL script not found for metric: " + metricName
-                    + ". Available: " + scriptMap.keySet());
+                "Transpiled MAL expression not found for metric: " + metricName
+                    + ". Available: " + scriptMap.size() + " expressions");
         }
 
         try {
-            Class<?> scriptClass = Class.forName(className);
-            DelegatingScript script = (DelegatingScript) 
scriptClass.getDeclaredConstructor().newInstance();
+            Class<?> exprClass = Class.forName(className);
+            MalExpression malExpr = (MalExpression) 
exprClass.getDeclaredConstructor().newInstance();
             int count = LOADED_COUNT.incrementAndGet();
-            log.debug("Loaded pre-compiled MAL script [{}/{}]: {}", count, 
scriptMap.size(), metricName);
-            return new Expression(metricName, expression, script);
+            log.debug("Loaded transpiled MAL expression [{}/{}]: {}", count, 
scriptMap.size(), metricName);
+            return new Expression(metricName, expression, malExpr);
         } catch (ClassNotFoundException e) {
             throw new IllegalStateException(
-                "Pre-compiled MAL script class not found: " + className, e);
+                "Transpiled MAL expression class not found: " + className, e);
         } catch (ReflectiveOperationException e) {
             throw new IllegalStateException(
-                "Failed to instantiate pre-compiled MAL script: " + className, 
e);
+                "Failed to instantiate transpiled MAL expression: " + 
className, e);
         }
     }
 
@@ -84,7 +79,7 @@ public final class DSL {
             Map<String, String> map = new HashMap<>();
             try (InputStream is = 
DSL.class.getClassLoader().getResourceAsStream(MANIFEST_PATH)) {
                 if (is == null) {
-                    log.warn("MAL script manifest not found: {}", 
MANIFEST_PATH);
+                    log.warn("MAL expression manifest not found: {}", 
MANIFEST_PATH);
                     SCRIPT_MAP = map;
                     return map;
                 }
@@ -96,16 +91,19 @@ public final class DSL {
                         if (line.isEmpty()) {
                             continue;
                         }
-                        String[] parts = line.split("=", 2);
-                        if (parts.length == 2) {
-                            map.put(parts[0], parts[1]);
+                        // mal-expressions.txt format: one FQCN per line
+                        // Class name convention: ...mal.MalExpr_<metricName>
+                        String simpleName = 
line.substring(line.lastIndexOf('.') + 1);
+                        if (simpleName.startsWith("MalExpr_")) {
+                            String metric = 
simpleName.substring("MalExpr_".length());
+                            map.put(metric, line);
                         }
                     }
                 }
             } catch (IOException e) {
-                throw new IllegalStateException("Failed to load MAL script 
manifest", e);
+                throw new IllegalStateException("Failed to load MAL expression 
manifest", e);
             }
-            log.info("Loaded {} pre-compiled MAL scripts from manifest", 
map.size());
+            log.info("Loaded {} transpiled MAL expressions from manifest", 
map.size());
             SCRIPT_MAP = map;
             return map;
         }
diff --git 
a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/EntityDescription/InstanceEntityDescription.java
 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/EntityDescription/InstanceEntityDescription.java
new file mode 100644
index 0000000..e047b67
--- /dev/null
+++ 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/EntityDescription/InstanceEntityDescription.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl.EntityDescription;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.apache.skywalking.oap.server.core.analysis.Layer;
+import org.apache.skywalking.oap.server.core.analysis.meter.ScopeType;
+
+/**
+ * GraalVM replacement for upstream InstanceEntityDescription.
+ * Change: Closure&lt;Map&gt; → Function&lt;Map, Map&gt; for 
propertiesExtractor.
+ */
+@Getter
+@RequiredArgsConstructor
+@ToString
+public class InstanceEntityDescription implements EntityDescription {
+    private final ScopeType scopeType = ScopeType.SERVICE_INSTANCE;
+    private final List<String> serviceKeys;
+    private final List<String> instanceKeys;
+    private final Layer layer;
+    private final String serviceDelimiter;
+    private final String instanceDelimiter;
+    private final Function<Map<String, String>, Map<String, String>> 
propertiesExtractor;
+
+    @Override
+    public List<String> getLabelKeys() {
+        return Stream.concat(this.serviceKeys.stream(), 
this.instanceKeys.stream()).collect(Collectors.toList());
+    }
+}
diff --git 
a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/Expression.java
 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/Expression.java
new file mode 100644
index 0000000..5d4fe5f
--- /dev/null
+++ 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/Expression.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.skywalking.oap.meter.analyzer.dsl;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * Same-FQCN replacement for upstream Expression.
+ * Wraps a transpiled {@link MalExpression} (pure Java) instead of a Groovy 
DelegatingScript.
+ *
+ * Upstream uses propertyMissing() delegate pattern + ExpandoMetaClass for 
Number ops.
+ * This replacement is much simpler: MalExpression.run() already has all 
sample lookups
+ * and arithmetic baked into the generated Java code.
+ */
+@Slf4j
+@ToString(of = {"literal"})
+public class Expression {
+
+    private final String metricName;
+    private final String literal;
+    private final MalExpression expression;
+
+    public Expression(final String metricName, final String literal, final 
MalExpression expression) {
+        this.metricName = metricName;
+        this.literal = literal;
+        this.expression = expression;
+    }
+
+    /**
+     * Parse the expression statically.
+     *
+     * @return Parsed context of the expression.
+     */
+    public ExpressionParsingContext parse() {
+        try (ExpressionParsingContext ctx = ExpressionParsingContext.create()) 
{
+            Result r = run(ImmutableMap.of());
+            if (!r.isSuccess() && r.isThrowable()) {
+                throw new ExpressionParsingException(
+                    "failed to parse expression: " + literal + ", error:" + 
r.getError());
+            }
+            if (log.isDebugEnabled()) {
+                log.debug("\"{}\" is parsed", literal);
+            }
+            ctx.validate(literal);
+            return ctx;
+        }
+    }
+
+    /**
+     * Run the expression with a data map.
+     *
+     * @param sampleFamilies a data map includes all of candidates to be 
analysis.
+     * @return The result of execution.
+     */
+    public Result run(final Map<String, SampleFamily> sampleFamilies) {
+        try {
+            SampleFamily sf = expression.run(sampleFamilies);
+            if (sf == SampleFamily.EMPTY) {
+                if (ExpressionParsingContext.get().isEmpty()) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("result of {} is empty by \"{}\"", 
sampleFamilies, literal);
+                    }
+                }
+                return Result.fail("Parsed result is an EMPTY sample family");
+            }
+            return Result.success(sf);
+        } catch (Throwable t) {
+            log.error("failed to run \"{}\"", literal, t);
+            return Result.fail(t);
+        }
+    }
+}
diff --git 
a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
index 35252da..cb18a22 100644
--- 
a/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
+++ 
b/oap-libs-for-graalvm/meter-analyzer-for-graalvm/src/main/java/org/apache/skywalking/oap/meter/analyzer/dsl/FilterExpression.java
@@ -17,8 +17,6 @@
 
 package org.apache.skywalking.oap.meter.analyzer.dsl;
 
-import groovy.lang.Closure;
-import groovy.lang.Script;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
@@ -30,25 +28,20 @@ import lombok.ToString;
 import lombok.extern.slf4j.Slf4j;
 
 /**
- * GraalVM replacement for upstream FilterExpression.
- * Original: 
skywalking/oap-server/analyzer/meter-analyzer/src/main/java/.../dsl/FilterExpression.java
- * Repackaged into meter-analyzer-for-graalvm via maven-shade-plugin (replaces 
original .class in shaded JAR).
- *
- * Change: Complete rewrite. Loads pre-compiled Groovy filter closure classes 
from
- * META-INF/mal-filter-scripts.properties manifest instead of GroovyShell 
runtime compilation.
- * Why: Groovy runtime compilation is incompatible with GraalVM native image.
+ * Same-FQCN replacement for upstream FilterExpression.
+ * Loads transpiled {@link MalFilter} classes from 
mal-filter-expressions.properties
+ * manifest instead of Groovy filter closures — no Groovy runtime needed.
  */
 @Slf4j
 @ToString(of = {"literal"})
 public class FilterExpression {
-    private static final String MANIFEST_PATH = 
"META-INF/mal-filter-scripts.properties";
+    private static final String MANIFEST_PATH = 
"META-INF/mal-filter-expressions.properties";
     private static volatile Map<String, String> FILTER_MAP;
     private static final AtomicInteger LOADED_COUNT = new AtomicInteger();
 
     private final String literal;
-    private final Closure<Boolean> filterClosure;
+    private final MalFilter malFilter;
 
-    @SuppressWarnings("unchecked")
     public FilterExpression(final String literal) {
         this.literal = literal;
 
@@ -56,22 +49,21 @@ public class FilterExpression {
         String className = filterMap.get(literal);
         if (className == null) {
             throw new IllegalStateException(
-                "Pre-compiled filter script not found for: " + literal
+                "Transpiled MAL filter not found for: " + literal
                     + ". Available filters: " + filterMap.size());
         }
 
         try {
-            Class<?> scriptClass = Class.forName(className);
-            Script filterScript = (Script) 
scriptClass.getDeclaredConstructor().newInstance();
-            filterClosure = (Closure<Boolean>) filterScript.run();
+            Class<?> filterClass = Class.forName(className);
+            malFilter = (MalFilter) 
filterClass.getDeclaredConstructor().newInstance();
             int count = LOADED_COUNT.incrementAndGet();
-            log.debug("Loaded pre-compiled filter script [{}/{}]: {}", count, 
filterMap.size(), literal);
+            log.debug("Loaded transpiled MAL filter [{}/{}]: {}", count, 
filterMap.size(), literal);
         } catch (ClassNotFoundException e) {
             throw new IllegalStateException(
-                "Pre-compiled filter script class not found: " + className, e);
+                "Transpiled MAL filter class not found: " + className, e);
         } catch (ReflectiveOperationException e) {
             throw new IllegalStateException(
-                "Failed to instantiate pre-compiled filter script: " + 
className, e);
+                "Failed to instantiate transpiled MAL filter: " + className, 
e);
         }
     }
 
@@ -79,7 +71,7 @@ public class FilterExpression {
         try {
             Map<String, SampleFamily> result = new HashMap<>();
             for (Map.Entry<String, SampleFamily> entry : 
sampleFamilies.entrySet()) {
-                SampleFamily afterFilter = entry.getValue().filter(tags -> 
filterClosure.call(tags));
+                SampleFamily afterFilter = 
entry.getValue().filter(malFilter::test);
                 if (!Objects.equals(afterFilter, SampleFamily.EMPTY)) {
                     result.put(entry.getKey(), afterFilter);
                 }
@@ -102,7 +94,7 @@ public class FilterExpression {
             Map<String, String> map = new HashMap<>();
             try (InputStream is = 
FilterExpression.class.getClassLoader().getResourceAsStream(MANIFEST_PATH)) {
                 if (is == null) {
-                    log.warn("Filter script manifest not found: {}", 
MANIFEST_PATH);
+                    log.warn("MAL filter manifest not found: {}", 
MANIFEST_PATH);
                     FILTER_MAP = map;
                     return map;
                 }
@@ -110,9 +102,9 @@ public class FilterExpression {
                 props.load(is);
                 props.forEach((k, v) -> map.put((String) k, (String) v));
             } catch (IOException e) {
-                throw new IllegalStateException("Failed to load filter script 
manifest", e);
+                throw new IllegalStateException("Failed to load MAL filter 
manifest", e);
             }
-            log.info("Loaded {} pre-compiled filter scripts from manifest", 
map.size());
+            log.info("Loaded {} transpiled MAL filters from manifest", 
map.size());
             FILTER_MAP = map;
             return map;
         }
diff --git a/pom.xml b/pom.xml
index 85d9fff..f55d62d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
              Do not hardcode — always build via `make`. -->
         <skywalking.version>10.4.0-SNAPSHOT</skywalking.version>
         <graalvm.version>25.0.0</graalvm.version>
+        <graalvm.buildtools.version>0.10.4</graalvm.buildtools.version>
         <lombok.version>1.18.40</lombok.version>
         
<maven-checkstyle-plugin.version>3.1.0</maven-checkstyle-plugin.version>
         <checkstyle.version>6.18</checkstyle.version>

Reply via email to