Skip to main content

suggest-native-expression

Use when picking the next Spark expression to implement natively in Comet, or when asked whether a specific codegen-dispatched expression is worth a native Rust implementation. Scores the candidate on how likely a native path can be 100% Spark-compatible and how much throughput or allocation it would save versus the JVM codegen dispatcher, records the verdict in the per-expression audit log so disqualified candidates are not re-litigated, and files a GitHub issue for the recommendation.

Source facts

Repository
apache/datafusion-comet
Last source activity
August 31, 2026 at 18:24
Detected SKILL.md language
English
Stars
1,290
Forks
391

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
suggest-native-expression
description
Use when picking the next Spark expression to implement natively in Comet, or when asked whether a specific codegen-dispatched expression is worth a native Rust implementation. Scores the candidate on how likely a native path can be 100% Spark-compatible and how much throughput or allocation it would save versus the JVM codegen dispatcher, records the verdict in the per-expression audit log so disqualified candidates are not re-litigated, and files a GitHub issue for the recommendation.
argument-hint
["expression-name"]
<!-- 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. --> Assess whether a Spark expression that Comet runs through the JVM codegen dispatcher is worth implementing natively in Rust, and propose the best candidate as a GitHub issue. This skill does **not** implement anything. Its deliverables are exactly three: 1. A verdict on two axes: **compatibility confidence** (can a native path be 100% Spark-compatible?) and **native upside** (how much throughput or allocation does the native path save versus codegen dispatch?). 2. A dated `Native candidate` line in the expression's page under `docs/source/contributor-guide/expression-audits/`, so a future run can see that this expression was already assessed and why it was or was not taken. 3. One filed GitHub issue for the recommended (or deferred-with-a-named-blocker) candidate. `$ARGUMENTS` may name a single expression to assess. If it is empty, run in survey mode (Step 1) and pick the best candidate from the whole pool. ## Background reading Read these before scoring anything. Do not score from memory: the routing model and the cost of the dispatcher are both easy to misremember, and every judgment in this skill depends on them. - `docs/source/contributor-guide/roadmap.md`, section "Native Coverage for Codegen-Dispatched Expressions". This is the project-level statement of why this work matters and it is the section the filed issue should reference. - `docs/source/user-guide/latest/compatibility/index.md`, section "Native and codegen-dispatch implementations". Defines the two paths and which one is the default. - `docs/source/user-guide/latest/expressions.md`, the Implementation legend and tables. Useful for a candidate's SQL name, category, and status, but see Step 1: this table is not the pool. - `docs/source/contributor-guide/optimizing_expressions.md`, the "Proven techniques" table. If no technique in that table applies to the candidate, the native upside claim is weak. - `spark/src/main/scala/org/apache/comet/serde/CometExpressionSerde.scala` for the `CometCodegenDispatch` / `CodegenDispatchFallback` / `NativeOptInAvailable` contracts. - `spark/src/main/scala/org/apache/comet/codegen/CometBatchKernelCodegen.scala` for what the dispatcher can and cannot compile (`isSupportedDataType`, `canHandle`). ## What the dispatcher actually costs Getting this right is the whole point of the skill. Overstating the dispatcher's cost produces issues that waste a contributor's week on a 2% win. What codegen dispatch **does** cost: - One JNI round trip per batch, out of native execution and back. - Per-row evaluation of Spark's generated Java. No vectorization, no SIMD, no dictionary awareness, no bulk buffer operations. - Per-row JVM heap allocation for every non-primitive intermediate and result: `UTF8String`, `Decimal`/`BigDecimal`, `GenericArrayData`, `ArrayBasedMapData`, boxed values, plus whatever Spark's own `doGenCode` allocates (formatters, `StringBuilder`, temporary arrays). - That allocation lands on the JVM heap, outside Comet's native memory pool, so it is invisible to Comet's memory accounting and it feeds GC pressure that shows up as task-level jitter rather than as an expression cost. - Per-row reads of nested Arrow data go through `CometArrayData` / `CometMapData` / `CometSpecializedGettersDispatch` wrappers, which allocate per row per nesting level. What codegen dispatch **does not** cost, and therefore is not available as an argument for going native: - It does **not** convert the whole batch to rows and back. The kernel reads Arrow vectors directly. - String reads are zero-copy (`UTF8String.fromAddress`). - The Janino kernel is compiled once per `(expression, schema)` pair, not per batch. - A `NullIntolerant` root short-circuits null rows without evaluating the body. - It is byte-exact with Spark by construction, so it needs no compatibility work at all. A native replacement starts with a compatibility debt that the dispatcher does not have. The corollary: a native implementation is worth proposing when the per-row JVM work is **allocation-heavy or vectorizable**, not merely because "native is faster than JVM". ## Step 1: Choose the candidate If `$ARGUMENTS` names an expression, skip to Step 2 but still run the exclusion checks below against it. ### Build the pool The **serde definitions are the source of truth**, not the docs table. Start there: ```bash # Sub-pool A: expressions whose only path is the dispatcher, plus those with a native opt-in. grep -rhoE "CometCodegenDispatch\[[A-Za-z0-9]+\]" spark/src/main/scala/org/apache/comet/serde/*.scala | sed 's/.*\[//;s/\]//' | sort -u # Which of those also have a native path (rendered "Hybrid" in the docs)? grep -rn "NativeOptInAvailable\|CodegenDispatchFallback" spark/src/main/scala/org/apache/comet/serde/*.scala ``` Then cross-reference `docs/source/user-guide/latest/expressions.md` for each candidate's SQL name, category, and Implementation label. Do **not** build the pool from that table alone: its Implementation column is derived from a function-registry lookup, so a dispatcher-routed serde whose SQL name does not resolve is rendered `—` and disappears from a table-driven grep. `mask` is the worked example: `CometMask extends CometCodegenDispatch[Mask]`, but its row reads `—`. At the time of writing the counts are 67 classes extending `CometCodegenDispatch` against 58 rows labelled `Codegen dispatch`, so a table-driven pool silently loses candidates. Two sub-pools, both in scope, with different proposed approaches: - **A. Dispatch-only** (a plain `CometCodegenDispatch`, no native path). The proposal is a new native kernel or an upstream wiring. - **B. Hybrid** (`NativeOptInAvailable` / `CodegenDispatchFallback`: a native path exists but is `Incompatible`, so **the dispatcher is still what runs by default**). These are in scope because the user-visible default is identical to sub-pool A, and so is the win from fixing it. The proposal is different: close the compatibility gap, or add a runtime guard that routes only the genuinely incompatible inputs back through the dispatcher, so the native path can become the default. The mechanics belong to `audit-comet-expression`; this skill's job is to establish that the win is worth someone's time. Roughly 42 expressions sit here, so skipping them would drop the largest block of candidates on a technicality. Out of scope: rows marked `—` or `🔜 Planned` where Comet has **no** path at all and the whole operator falls back to Spark. Those are `implement-comet-expression` territory, because the question is "implement it at all" rather than "is native better than dispatch". Their upside is genuinely larger than anything in this pool, so if you are looking for the biggest win available, say so and point the user at that skill rather than silently ranking within this pool as if it were the whole space. ### Exclude 1. **Families the project has ruled out.** Read the "Not currently planned" section of `docs/source/user-guide/latest/expressions.md` and drop anything it covers. Proposing one of these needs a project-direction argument that this skill does not have. 2. **Already-assessed expressions.** This is the check that makes the skill cumulative: ```bash grep -rn "Native candidate (assessed" docs/source/contributor-guide/expression-audits/ ``` An expression with a `Disqualified` line is off the pool unless the **named blocker** in that line has demonstrably changed (an upstream kernel landed, DataFusion gained the function, the Spark semantics were simplified in a new version). If it has changed, say so explicitly in the new assessment and add a new dated line. Never delete or rewrite an old line. 3. **Expressions with an existing issue.** Search open and closed: ```bash gh issue list --repo apache/datafusion-comet --search "<function> native in:title,body" --state all --limit 10 ``` If a candidate match comes back, open it (`gh issue view <N> --repo apache/datafusion-comet`) and confirm it really covers a native implementation of this expression. If it does, that expression is done: pick another rather than filing a duplicate. ### Rank what is left Rank the remaining pool by a cheap first-pass read of the two axes below, then assess the top candidate properly. Prefer expressions that appear in TPC-H or TPC-DS, or in common ETL shapes (string cleanup, date bucketing, JSON field extraction), over expressions that are rare in real queries. To check workload presence: ```bash grep -rli "<function>(" spark/src/test/resources/tpcds/ spark/src/test/resources/tpch/ 2>/dev/null ``` State the pool size, the exclusions, and why the chosen candidate topped the ranking. A ranking you do not show is a ranking the reader cannot challenge. ## Step 2: Establish how the expression runs today Find the serde and read it: ```bash grep -rn "class\|object" spark/src/main/scala/org/apache/comet/serde/*.scala | grep -i "<Expr>" ``` Record: - Which trait it uses, which decides the sub-pool and therefore the shape of the proposal. A plain `CometCodegenDispatch[T]` means dispatch is the only path (sub-pool A). `NativeOptInAvailable` or `CodegenDispatchFallback` means a native path already exists but is off by default (sub-pool B), so the candidate is not "write a kernel" but "make the existing kernel the default": establish which inputs actually diverge, and whether the divergence is detectable well enough to guard. Read the existing `getIncompatibleReasons()` and treat each listed reason as a claim to verify, not a given. - Whether the dispatcher can actually compile it, per `CometBatchKernelCodegen.canHandle` and `isSupportedDataType`. If some input or output type is outside that surface, those cases fall back to **Spark**, not to the dispatcher. Whole-operator Spark fallback is a much larger cost than a JNI round trip, and it raises the upside score materially. Say which cases these are. - Whether the expression is disabled or gated by any config (`spark.comet.expression.<Name>.enabled`, `allowIncompatible`, `spark.comet.exec.scalaUDF.codegen.enabled`). ## Step 3: Score compatibility confidence The question is narrow: **can a native Rust implementation match Spark bit-for-bit, for every input, on every supported Spark version?** "Close enough" is not a passing answer, because the dispatcher it would replace is exact. ### Read the Spark implementation across versions Derive the version list from the build rather than hardcoding it, so the assessment does not go stale as Comet drops and adds Spark support: ```bash grep -oE "<spark.version>[0-9.]+</spark.version>" pom.xml | sed -E 's/<[^>]+>//g' | sort -u ``` Read `eval` / `nullSafeEval` / `doGenCode`, `inputTypes`, `dataType`, ANSI branches, and any config the expression reads, for each of those versions. The class body is usually short enough to fetch directly instead of cloning: ```bash curl -sSfL "https://raw.githubusercontent.com/apache/spark/v<version>/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/<file>.scala" ``` Diff the class body between adjacent versions rather than eyeballing it. Note behavior that differs: each divergence is a shim the native path would need, and "identical across all supported versions" is itself a finding worth stating, because it means no shim. ### Check upstream first An existing upstream implementation is the single strongest positive signal, both for compatibility and for effort: ```bash grep -rn "fn name" ~/.cargo/registry/src/*/datafusion-spark-*/src/function/ 2>/dev/null | grep -i "<function>" grep -rn "fn name" ~/.cargo/registry/src/*/datafusion-functions-*/src/ 2>/dev/null | grep -i "<function>" ``` If `datafusion-spark` has it, read the implementation and diff its semantics against Spark's before crediting it. Upstream functions in that crate aim for Spark compatibility but are not all complete. If it matches, the proposal becomes a wiring job (`wire-datafusion-function`), which is a much cheaper issue to file and a much easier one to pick up. ### Hazard checklist Each hazard that applies is a strike against compatibility confidence. Check every one and report the result, including the ones that do not apply, so the reader can see the checklist was run rather than skimmed. | Hazard | Why it threatens 100% compatibility | | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | JVM formatting APIs (`java.text.DecimalFormat`, `String.format`, `SimpleDateFormat`, `DateTimeFormatter`) as in `format_number`, `format_string`, `to_char` | Locale, pattern, and rounding behavior would have to be reimplemented, not merely ported | | `java.util.regex` features (backreferences, lookaround, possessive quantifiers, named groups) | The Rust `regex` crate deliberately omits these, so some patterns cannot be supported at all | | Collation (`StringTypeWithCollation`, `CollationSupport.X.exec`) on Spark 4.0+ | ICU-backed collation is not propagated natively today ([#4496](https://github.com/apache/datafusion-comet/issues/4496)) | | `BigDecimal` / `MathContext` precision and rounding, precision-scale promotion, overflow-to-null | Rounding mode and overflow points are easy to get subtly wrong and produce silent wrong results | | Timezone and calendar (JDK tzdb versus `chrono-tz`, DST edges, hybrid Julian/Gregorian rebase, `spark.sql.legacy.*` flags) | Divergences appear only on specific dates and zones, so tests rarely catch them | | ANSI error class and message parity | An error with the wrong class or message is an observable difference; see the ANSI section of the compatibility guide | | Float and double text conversion (`Double.toString` shortest round-trip), `StrictMath` versus `f64` intrinsics | Last-digit differences on string output, platform-dependent differences on transcendentals | | Raw byte semantics of `UTF8String` (invalid UTF-8) ([#4764](https://github.com/apache/datafusion-comet/issues/4764)) | Arrow requires valid UTF-8, so byte-preserving behavior cannot be reproduced | | Lambda bodies or arbitrary child expression trees (`transform`, `aggregate`, `zip_with`) | The native path would need to evaluate an arbitrary Catalyst tree, not a fixed kernel | | Dependence on JVM session state beyond a scalar config (SQLConf lookups, user classes, reflection) | State that cannot be serialized into the plan cannot cross into native code | | Nondeterminism or ordering-sensitivity (hash iteration order, unstable sorts) | Output order must match Spark's, which is often an implementation detail rather than a spec | | Behavior that differs across the Spark versions pinned in `pom.xml` | Each divergence is a shim; several divergences means the native path carries version branches forever | ### Rate it - **High**: no hazard applies, or every hazard that applies is fully specified and mechanically reproducible (for example integer arithmetic with a documented overflow rule). A reviewer reading the Spark source could enumerate every case. - **Medium**: hazards apply but are bounded and detectable **from the expression or the batch at runtime**, so the incompatible cases can be routed back through the dispatcher via `CodegenDispatchFallback` while the common case runs natively. Name the guard and say how it is detected. "We will document the difference" is not a guard. - **Low**: matching Spark requires reimplementing a JVM library (ICU collation, `DecimalFormat`), or the incompatible cases cannot be detected before evaluating, or the only honest native path is `Incompatible` by default. A `Low` rating is a hard disqualifier regardless of upside. The project's direction is the opposite of shipping incompatible-by-default native paths (see [#4506](https://github.com/apache/datafusion-comet/issues/4506) and the discussion in [#4654](https://github.com/apache/datafusion-comet/issues/4654)). Verify both issues are still open before citing them. ## Step 4: Score native upside Read Spark's `doGenCode` (and `nullSafeEval`) for the expression and answer, concretely: **what does the JVM allocate or recompute per row, and can a Rust kernel avoid it?** Positive signals, strongest first: 1. **Per-row heap allocation in the generated code.** Count the allocation sites: `new UTF8String`, `UTF8String.fromString`, `.getBytes()`, `Decimal.apply`, `new GenericArrayData`, `new StringBuilder`, boxing. Multiply by batch size (8192) to state the allocation the native path removes. This is the most defensible upside argument, and it is the one that maps directly to the "reduction in memory allocation" question. 2. **Work that is constant across the batch but repeated per row.** A regex compiled per row, a formatter constructed per row, a literal argument re-parsed per row. Native kernels hoist this (see the technique table). 3. **A technique from `optimizing_expressions.md` that plainly applies.** Vectorized `unary`/`binary` kernels, zero-copy buffer reuse, `Cow` borrow, an ASCII byte-offset fast path, a lookup table, direct offset-buffer reads. Name the technique and the shape it helps. 4. **Nested or complex input/output types.** Per-row reads of `ListVector` / `StructVector` / `MapVector` through the JVM wrappers allocate per row per level. Native code operates on the child arrays and offsets directly. 5. **Specific inputs of an in-pool expression that fall back to Spark entirely today** (found in Step 2: an input or output type outside `isSupportedDataType`, so the dispatcher cannot compile the kernel and the whole operator falls back). Recovering those inputs dwarfs removing a JNI
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub