All findings · Lucene finding · LFV-181 · expressions-boolean-operand-long-narrowed-to-int

Expression boolean operators keep only the low 32 bits of a long operand: (1 << 32) ? 1 : 0 is 0

In Lucene expressions, !, &&, || and ?: see only the low 32 bits of a bitwise result or hex literal: (1 << 32) ? 1 : 0 is 0, yet (1 << 32) != 0 is 1. High-bit flag tests on long fields are silently wrong since 4.6.0.

Summary

Priority
Low, rank 866Expression boolean operators keep only the low 32 bits of long operands, so high-bit flag tests silently read false, a specific idiom that a quick test reveals.
Impact
silent wrong results
Since
4.6.0 (the module's first release, LUCENE-5207, 2013)introduced 2013-09-13 · over 13 years
Affected
4.6.0 to 10.6.0 and main
Status
documented
Found by
modelling the compiler for an exhaustive check of small expressions

AI-assisted page: this finding was first signalled by a semi-formal check (an exact check of the real jar, or a modelling hypothesis that a tool then witnessed) and then checked with AI tools; the checks it went through, and what each found, are under Prior reports, history and checks. Not yet triaged by a human. Snapshot of 2026-11-06.

The finding in 30 seconds

Lucene compiles an expression to JVM bytecode and gives each sub-expression a type: int, long or double. Bitwise and shift results and hex literals are 64-bit longs. The boolean operators ask for an int, and turning a long into an int keeps only its low 32 bits.

Background in 3 steps

  1. The optional lucene-expressions module compiles a formula such as (flags & 0x100000000) ? 10 : 1 into a class whose evaluate returns a double per document; it is used for scores, sort keys and filters.
  2. The syntax is based on JavaScript, but in Lucene & | ^ ~ << >> >>> and hex/octal literals work on 64-bit longs: its own tests check -15 << 62 = 4611686018427387904 and 0x1234ABCDEF = 78193085935.
  3. !, &&, || and ?: treat 0 as false and any non-zero integer as true: the tests check !2 = 0 and -1 ? 2 : 3 = 2.
Diagram of the minimal repro: the expression (1 << 32) ? 1 : 0. The shift gives the 64-bit value 4294967296, with only bit 32 set. As the condition of ?:, the value is narrowed to a 32-bit int by keeping its low 32 bits, which are all zero, so the condition is false and the result is 0 instead of 1. Written as the decimal literal 4294967296, the same number stays true.
1 << 32 is the 64-bit value 4294967296: bit 32 alone. The boolean operator keeps only the low 32 bits, which are all zero, so the condition counts as false. Select the still to open it at full size. Generated with AI, checked against the code.

The finding, step by step

  1. Compile (1 << 32) ? 1 : 0 with JavascriptCompiler and evaluate it. In Lucene expressions shifts and & work on 64-bit longs, so 1 << 32 is 4294967296, and any non-zero integer is true (!2 is 0, -1 ? 2 : 3 is 2).
  2. You expect 1: the condition is 4294967296, and (1 << 32) != 0 does give 1.
  3. You get 0, with no error. The compiler asks for the condition as a 32-bit int and narrows the 64-bit value by keeping its low 32 bits. 4294967296 is bit 32 alone, so all 32 kept bits are zero. !, &&, || and hex literals such as 0x100000000 behave the same.
  4. The same number written another way is true: 4294967296 ? 1 : 0 and ((1 << 32) + 0) ? 1 : 0 give 1. Decimal numbers and arithmetic are doubles, and a double too large for an int narrows to the largest int, which is still non-zero.
  5. On a long doc-values field, (flags & 0x100000000) ? 10 : 1 returns 1 for a document with flags = 4294967297 (bits 32 and 0 set). A single-bit mask test such as (flags & 0x100000000) used directly as a condition is false for every document; the same holds for each of bits 32 to 63.

Who is affected

Users of the expressions module who apply !, &&, || or ?: directly to a bitwise result, typically to test a bit from 32 to 63 of a long doc-values field: (flags & 0x100000000) ? 10 : 1. The condition is false for every document, so documents with the bit set silently get wrong scores, sort keys or filter results.

Mechanism, terms and numbers856 words

Lucene expressions compile a formula to bytecode, giving every sub-expression a JVM type: int, long or double. The bitwise and shift operators and hex/octal literals are 64-bit longs (1 << 32 is 4294967296), but the four boolean operators !, &&, || and ?: request their operand as an int and narrow a long with l2i, which keeps only the low 32 bits. So a non-zero long whose low 32 bits are zero counts as false: (1 << 32) ? 1 : 0 and 0x100000000 ? 1 : 0 are 0, !(1 << 32) is 1, while (1 << 32) != 0 is 1 and the same number written in decimal is true.

The practical case is a flag test of bits 32 to 63 of a long doc-values field: (flags & 0x100000000) ? 10 : 1 returns 1 and !(flags & 0x100000000) returns 1 for a document with bit 32 set, so the condition is false for every document, and documents with the bit set get the wrong value in scores, sort keys and expression filters, with no error. Bits 0 to 31 and the comparison forms (e) != 0 and (e) == 0 are correct.

The int operand is in the module's first patch (LUCENE-5207, 2013): all 122 lucene-expressions releases from 4.6.0 to 10.6.0 fail, and so does main. Not fixed upstream. Fix: evaluate the operand as a long and compare it with 0L, which changes nothing for double operands.

The mechanism (main 645d4fe14a20)

  1. The code generator is type-directed. Each visitor of JavascriptCompiler pushes the type its parent expects onto typeStack, and each child converts its result to typeStack.peek() with gen.conversion(...).
  2. The boolean operators ask for an int. ! pushes TypeKind.INT for its operand (visitUnary, JavascriptCompiler.java:518) and branches with ifeq (:521); && (:694, ifeq at :696, :698), || (:714, ifne at :716, :718) and the ?: condition (:734, ifeq at :737) do the same.
  3. Bitwise and shift results are longs, narrowed to the expected type. pushShift and pushBitwise compute in long (:751-757), and pushBinaryOp ends with gen.conversion(returnType, typeStack.peek()) (:773): long to int is l2i. ~ does the same (:534). A hex or octal literal goes through pushLong, which pushes (int) i in an int position (:796).
  4. l2i and (int) keep the low 32 bits. 4294967296 = 2^32 has only bit 32 set, so it becomes 0 and ifeq takes the false branch. In general a non-zero long operand is false exactly when its low 32 bits are zero (checked with an SMT solver); a single-bit test (x & (1 << k)) is false for every x when 32 <= k <= 62 (solver) and for bit 63 (executed: (1 << 63) ? 1 : 0 is 0).
  5. Doubles of magnitude 1 or more are not affected. A decimal literal (:438-439), a variable (:503), a function result (:494) or arithmetic is a double, narrowed with d2i, which saturates: 4294967296.0 becomes Integer.MAX_VALUE, still non-zero. That is why 4294967296 ? 1 : 0 and ((1 << 32) + 0) ? 1 : 0 are 1. (d2i also makes a fraction false; see the fractions note below.) Comparisons evaluate both sides as doubles (:650): a long result reaches (e) != 0 through l2d, which is non-zero for every non-zero long.

Requesting the four operands as long and comparing them with 0L makes the repro and every row below correct, so the narrowing is the whole cause.

The executed forms (main and 10.5.2, assertions off)

expressionoperand valueresultconsistent answer
(1 << 32) ? 1 : 0429496729601
0x100000000 ? 1 : 0429496729601
!(1 << 32)429496729610
(1 << 32) && 1429496729601
(x & 0x100000000) ? 1 : 0, x = 4294967297429496729601
!(x & 0x100000000), x = 4294967297429496729610
4294967296 ? 1 : 0 (decimal literal)429496729611
((1 << 32) + 0) ? 1 : 0429496729611
x ? 1 : 0, x = 4294967296 (a variable)429496729611
(x & 0x100000000) != 0, x = 4294967297429496729611
(1 << 31) ? 1 : 0 and (x & 1) ? 1 : 0low bits set11

(1 << 32) || 0 is 0 as well (expected 1). Over an indexed NumericDocValuesField with flags = 4294967297, (flags & 0x100000000) ? 10 : 1 gives 1 instead of 10 and !(flags & 0x100000000) gives 1 instead of 0.

The same code site: fractions

The same int operand also makes a fractional double false, through d2i: with x = 0.5, x ? 1 : 0 is 0 while x != 0 is 1. No document states a truth rule for fractions either way, so that case was judged a documentation question rather than a code bug. Fix B below leaves it unchanged; fix A changes it too.

Exposure and likelihood401 words
  • Silent wrong results. The expression returns a number and nothing fails. Scores (getDoubleValuesSource in a FunctionScoreQuery), sort keys (getSortField), rescoring and expression-based filters (FunctionMatchQuery over the values source) take the false branch even where the tested bit is set.
  • Who is exposed. Users of lucene-expressions at default settings, any index size, assertions on or off, who apply a boolean operator directly to a bitwise or shift result or a hex/octal literal that is non-zero with all-zero low 32 bits. In practice: a test of one of bits 32 to 63 of a long bitmask field, written (flags & MASK) ? a : b or !(flags & MASK). Such a single-bit test is false for every document.
  • Expressions are the suggested tool for bitwise flag tests. On #3534 (an open feature request for bitwise filtering on numeric fields), gsmiller suggested in 2024 using expressions over a numeric doc-values field "since the compiler supports bitwise operations"; the comment names no particular form (?:, ! or a comparison). Downstream users write flag tests in expressions too (elastic/elasticsearch#26272, with small values).
  • Filter effect, executed. A FunctionMatchQuery with predicate v != 0 over 5 documents with flags = 0, 1, 2^32, 2^32 | 1 and 2^40: !(flags & 0x100000000) matches 5 of 5 instead of 3, (flags & 0x100000000) ? 1 : 0 matches 0 instead of 2, and the == 0 / != 0 forms match 3 and 2, correctly (main with assertions on and off, 10.5.2).
  • Not affected. Bits 0 to 31, the comparison forms (e) != 0 and (e) == 0, a decimal literal, variable or arithmetic result of magnitude 1 or more used directly as the operand (a fraction is the separate fractions case above), and the index itself.
  • A separate limit, not this bug. A long doc-values field reaches an expression as a double (DoubleValuesSource.fromLongField returns (double) v), so a field value above 2^53 in magnitude can lose low bits before & runs: for flags = 2^53 + 1, (flags & 1) != 0 is 0, before and after fix B. Values up to 2^53, such as every example here, are exact.
  • How likely. Rare: it needs a high-bit test written without a comparison. A single high-bit test is false for every document, so a quick test shows it, and no public report was found in 13 years of releases.

Minimal repro

One JUnit test in lucene/expressions, public API only: compile (1 << 32) ? 1 : 0 and assert that it evaluates to 1. No index and no randomness; it fails with assertions on and off and needs no build change.

TestBooleanOperandHighBits.java (1 test)29 lines
/*
 * 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.lucene.expressions.js;

import org.apache.lucene.tests.util.LuceneTestCaseJupiter;
import org.junit.jupiter.api.Test;

public class TestBooleanOperandHighBits extends LuceneTestCaseJupiter {

  @Test
  public void testConditionOnOneShiftedLeft32() throws Exception {
    // Expected 1: (1 << 32) is 4294967296, non-zero; actual 0: only its low 32 bits are tested.
    assertEquals(1.0, JavascriptCompiler.compile("(1 << 32) ? 1 : 0").evaluate(null), 0.0);
  }
}
Command
cd $LUCENE   # an apache/lucene checkout at 645d4fe14a20 (or a later commit where JavascriptCompiler.java is unchanged)
cp $REPRO/TestBooleanOperandHighBits.java lucene/expressions/src/test/org/apache/lucene/expressions/js/
./gradlew :lucene:expressions:test --tests org.apache.lucene.expressions.js.TestBooleanOperandHighBits -Ptests.seed=DEADBEEF
# assertions off: add -Ptests.asserts=false
Expected
(1 << 32) ? 1 : 0 evaluates to 1: its condition, 4294967296, is non-zero.
Observed
expected:<1.0> but was:<0.0> on main (assertions on and off), 10.6.0, 10.5.2 and 9.12.3.

All files of this repro: repro/LFV-181/

More files, environment, full output142 words

The test sits in org.apache.lucene.expressions.js next to the compiler's other tests and uses only JavascriptCompiler.compile and Expression.evaluate(null), as TestJavascriptOperations does for an expression without variables. The result does not depend on the seed (it fails with DEADBEEF and 5EED1234).

LuceneTestCaseJupiter exists only on main. For the release jars, the same assertion in a class that extends org.apache.lucene.tests.util.LuceneTestCase (no @Test annotation) fails unchanged on 10.6.0, 10.5.2 and 9.12.3; the history sweep ran a main() port on every release from 4.6.0.

Environment

lucenemain 645d4fe14a20: affected; the LuceneTestCase port also fails on the 10.6.0, 10.5.2 and 9.12.3 release jars
jdkOpenJDK 25.0.3 (build 25.0.3+9-LTS)
osLinux, aarch64

Observed output

TestBooleanOperandHighBits > testConditionOnOneShiftedLeft32() FAILED
    java.lang.AssertionError: expected:<1.0> but was:<0.0>
        at __randomizedtesting.SeedChain.seed([DEADBEEF:FB778992B7FCCC6A:80DC09A7BA44E00D])
        at junit@4.13.2/org.junit.Assert.fail(Assert.java:89)
        at junit@4.13.2/org.junit.Assert.failNotEquals(Assert.java:835)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:555)
        at junit@4.13.2/org.junit.Assert.assertEquals(Assert.java:685)
        at org.apache.lucene.expressions.js.TestBooleanOperandHighBits.testConditionOnOneShiftedLeft32(TestBooleanOperandHighBits.java:27)
4 tests completed, 1 failed   # the other 3 are LuceneTestCaseJupiter's built-in class checks

Why it looks unintended

  • Lucene's own tests say non-zero integers are true (!2 = 0) and shifts are 64-bit (-15 << 62).
  • Same value, different answer: 0x100000000 ? 1 : 0 = 0 but 4294967296 ? 1 : 0 = 1.
  • No document or recorded decision states a 32-bit test; JavaScript also gives 1 for 0x100000000 ? 1 : 0.
The full argument252 words
  • The tests pin both halves of the conflict. Non-zero integers other than 1 are true: !2 = 0, !-1 = 0 (TestJavascriptOperations.java:178-184), -1 ? 2 : 3 = 2 (:214). The bitwise operators and hex literals are 64-bit: -15 << 62 = 4611686018427387904 (:238), -1 >>> 37 = 134217727 (:260), 0x1234ABCDEF = 78193085935 (:331). 4294967296 is a non-zero integer that the module itself produces, yet it counts as false.
  • The result depends on how the number is written. 0x100000000 ? 1 : 0 = 0 but 4294967296 ? 1 : 0 = 1; (1 << 32) ? 1 : 0 = 0 but ((1 << 32) + 0) ? 1 : 0 = 1 and (1 << 32) != 0 = 1. A sub-expression bound by name through SimpleBindings.add(String, Expression) gives 1 where the same text inlined gives 0.
  • The docs list the operators side by side. The package javadoc lists hex and octal literals, the bitwise operators | & ^ ~ << >> >>> and the boolean operators && || ! ?: in "an expression syntax that's based on JavaScript expressions" (package-info.java:21-28). No javadoc, CHANGES or MIGRATE entry says the boolean operators see only 32 bits.
  • No recorded rationale. The int operand is in the first patch (LUCENE-5207, 2013) and survived four rewrites; no test applies a boolean operator to a long of 2^32 or more, and none of the reviewed commit messages, code comments and discussions mentions it.
The devil's-advocate check185 words

The strongest case for "intended" (an independent reviewer from another model family, rated moderate): JavascriptCompiler and Expression are @lucene.experimental (JavascriptCompiler.java:111), no document defines how a number becomes a boolean, the int lowering is 13 years old, and JavaScript's own & is 32-bit, so JavaScript also takes the false branch for (flags & 0x100000000) ? a : b.

Why it fails: Lucene deliberately makes <<, & and hex literals 64-bit (its own tests), so JavaScript's 32-bit & cannot excuse the result; for (1 << 32) ? 1 : 0 and 0x100000000 ? 1 : 0 JavaScript returns 1 as well (a reference run in node 24; for the first only because JavaScript takes shift counts mod 32, so its 1 << 32 is 1), so Lucene matches neither JavaScript nor 64-bit values; experimental allows API changes, not a non-zero value that is false in one spelling and true in another; and no test, commit or discussion chose the truncation. Verdict: a code bug (every verifier, the cross-model reviews and the adjudication say real; the adjudication and the cross-model reviews with high confidence).

Suggested fix

Recommended B: evaluate the boolean operand as a long and compare it with 0L

Evaluate the operand of !, &&, || and ?: as a long and compare it with 0L (a small pushTruth helper used at the four sites). Double operands keep their current truth, so only the broken cases change. The repro passes with assertions on and off; all 170 expressions tests (assertions on) and the module's format and lint checks pass.

  private void pushTruth(ExpressionContext operand) {
    typeStack.push(TypeKind.LONG);
    visit(operand);
    typeStack.pop();
    gen.lconst_0();
    gen.lcmp();
  }

  // in each of the four visitors, for example visitConditional:
- typeStack.push(TypeKind.INT);
- visit(ctx.expression(0));
- typeStack.pop();
+ pushTruth(ctx.expression(0));
  gen.ifeq(condFalse);
All options and trade-offs3 options ยท 572 words

Two options were built and validated through gradle on 645d4fe14a20; a third is listed only to show why it is not recommended.

B: evaluate the boolean operand as a long and compare it with 0L (validated by the checks) (recommended)

A helper replaces the typeStack.push(TypeKind.INT) / visit / pop sequence in visitUnary (BOOLNOT), visitBooland, visitBoolor and visitConditional; the ifeq/ifne branches stay as they are (+19/-14 in JavascriptCompiler.java). With LONG expected, bitwise and shift results and hex/octal literals arrive with all 64 bits, nested boolean results arrive as 0L/1L, and lcmp against 0L leaves 0 exactly when the operand is 0.

  private void pushTruth(ExpressionContext operand) {
    typeStack.push(TypeKind.LONG);
    visit(operand);
    typeStack.pop();
    gen.lconst_0();
    gen.lcmp();
  }

  // in each of the four visitors, for example visitConditional:
- typeStack.push(TypeKind.INT);
- visit(ctx.expression(0));
- typeStack.pop();
+ pushTruth(ctx.expression(0));
  gen.ifeq(condFalse);

Trade-offs

  • Only the broken cases change. A double operand is now converted with d2l instead of d2i; both give 0 for exactly the same doubles (NaN and -1 < v < 1), so decimal literals, variables, functions and arithmetic keep their truth. Executed: 0.5 ? 1 : 0, !0.5, -0.5 ? 1 : 0, NaN, -0, 1e300 and the shift count in 1 << (0x100000000 | 2) give the same values as before.
  • It leaves the fractional case as it is: 0.5 ? 1 : 0 stays 0.
  • One lcmp per operand; no benchmark was run. The shift count, a separate int-typed position (pushShift, :751-753), is not touched.

What the checks validated

  • Gradle, scratch worktree of 645d4fe14a20, seed DEADBEEF: the repro fails on stock and passes with fix B, assertions on and off.
  • The whole :lucene:expressions:test with fix B and the repro (assertions on): 14 suites, 174 tests, 0 failures, 0 skipped (the 13 upstream suites with 170 tests, including TestJavascriptOperations and TestJavascriptCompiler, plus the repro).
  • :lucene:expressions:check -x test (checkGoogleJavaFormat, ecjLintMain, ecjLintTest, forbiddenApisMain, forbiddenApisTest): passes.
  • The 17-row probe (the table above) with fix B loaded: 0 rows wrong, 7 on stock.

A: evaluate the operand as a double, true if and only if abs(v) > 0 (validated by the checks)

The finder's sketch: a helper evaluates the operand with TypeKind.DOUBLE, calls Math.abs, compares with 0.0 via dcmpl (so NaN stays false) and branches with ifgt/ifle. A long operand reaches it through l2d, which is non-zero for every non-zero long.

Trade-offs

It also makes fractions true: 0.5 ? 1 : 0 goes from 0 to 1, !0.5 from 1 to 0 (NaN, -0 and 0 stay false). That would settle the fractional case as well, but it changes the results of existing expressions for 0 < |v| < 1, so it would need a CHANGES/MIGRATE note and is a policy decision for maintainers.

What the checks validated

The whole :lucene:expressions:test with fix A and the repro: 174 tests, 0 failures; the 17-row probe: 0 rows wrong. The format and lint checks were not run for A.

C (not recommended): saturating long-to-int narrowing

Convert long to int through double in pushBinaryOp and ~, and push (int)(double) for literals.

Trade-offs

It fixes the repro and passes the 170 tests, but it also changes long-typed shift counts of 2^31 or more: 1 << (0x100000000 | 2) goes from 4.0 to about -9.22e18, which no upstream test covers. A and B leave it at 4.0.

Downstream note

An application that writes an exclusion filter as an expression over flag bits 32 to 63, such as a FunctionMatchQuery on !(flags & 0x100000000), matches the documents it meant to exclude (5 of 5 instead of 3 in a probe); writing (flags & MASK) == 0 instead avoids it.

How it was found

Found while modelling the compiler's code generator for a bounded-exhaustive check of all 702,226 expression trees up to size 5; the model, faithful to the compiler, reads boolean operands as an int. Of the 237,419 evaluations that differ from exact-value semantics (first literal alphabet), 145,857 are this case; an SMT solver then characterised it.

The method in full200 words

Method: semi-formal (a hunt unit, the expressions compiler).

  • The finder, an AI tool, wrote an exact model of JavascriptCompiler's type-directed code generation, including the int type of boolean operands, and a bounded-exhaustive translation-validation harness: every expression tree of size up to 5 (702,226 trees, 13,147,954 evaluations per literal alphabet, two literal alphabets, two variables bound to 13 values), each compiled result compared bit for bit with the model (0 mismatches, so the model is faithful) and with a value-semantics reference (over all trees up to size 5 with the first literal alphabet, 237,419 evaluations differ, 145,857 of them a boolean operator over a long operand).
  • Z3 bit-vector and floating-point queries then characterised the class: a non-zero long operand is false exactly when its low 32 bits are zero; the same value through the double path is never false; a single-bit test of bit 32 to 62 is false for every input (with a vacuity probe for bits below 32); bit 63 was later shown the same way by execution.
  • The finder then wrote a public-API JUnit on the stock jar. Independent reproducers wrote their own tests from the claim alone; the one-test repro was cut from those on 2026-10-29.

Prior reports, history and checks

Prior reports

No prior report: 277 recorded searches over GitHub, the mailing lists, JIRA, downstream repositories and the web, plus the files of all 410 open PRs (2026-10-31). Closest: #3534, where a committer suggests expressions for bitwise filtering on numeric fields, the use this bug breaks for bits 32 and up.

Searches4 searches

Verdict new: none of the searched sources reports or discusses boolean operators on long-valued operands or the low-32-bit narrowing in Lucene expressions, and no fix exists. Related, not reports of this bug: #3534 (LUCENE-2460, open: bitwise filtering; gsmiller recommends expressions for it); elastic/elasticsearch#26272 (a downstream flag test that failed through operator precedence, values 4 and 7); #6271 (LUCENE-5207, the origin, 88 comments, none on operand typing); #7476, #12873 and #14597 (rewrites that kept the behaviour); #16423 and #16549 (recent compiler changes to other code); apache/lucenenet#996 and #1048 (shift and bitwise fixes in the .NET port, not the boolean operators). Gaps: GitHub search is lexical, and private reports are not visible.

What was searched, when, and what turned up
WhereWhenHowFound
GitHub issues, PRs, commits and code2026-10-31the scripted prior-report search (60 issue/PR, 6 commit and 5 downstream queries in its second run); 100 GraphQL searches: class and method names (visitConditional, visitBooland, pushBitwise, pushLong, typeStack), label:module:expressions, bitwise, ternary, truthiness, hex, octal, shift, 64-bit, 0x100000000, 4294967296; apache/lucene, elastic/elasticsearch, OpenSearch, apache/solr, apache/lucenenet and all of GitHub; 15 REST queriesonly the related items above
Open PRs and recent activity2026-10-31the changed files of all 410 open apache/lucene PRs; all 145 items updated since 2026-10-22only #16847 and #681 touch lucene/expressions, neither JavascriptCompiler.java
Mailing lists, JIRA, web2026-10-31dev@, java-user@, issues@ and commits@ (40 tool queries and 19 more); 9 JQL queries over LUCENE and SOLR including all 11 issues in modules/expressions; 18 web queriesno report
Earlier pass2026-10-26183 logged queries over the same sources; 22 apache/lucene items, 8 downstream items and 2 Solr JIRA issues read in fullverdict new

History

Introduced with the module by LUCENE-5207 (initial patch c73f175c71ee, 2013; first release 4.6.0). Executed: all 122 lucene-expressions releases from 4.6.0 to 10.6.0 fail, and so do the initial patch and main. 4.5.1 has no expressions module. Not fixed upstream.

Versions tested6 rows

Introduced by LUCENE-5207 "lucene expressions module" (#6271; SVN era, no PR): the initial patch c73f175c71ee "LUCENE-5207: initial patch from ryan" (2013-09-13, on the lucene5207 branch; patch by rjernst, committed by rmuir) already requested the boolean operands as int, narrowed long results with L2I and pushed int-position hex/octal literals as (int). It reached trunk as eecd78f37a12 and branch_4x as 08df432f8ba2 (2013-09-15). Later rewrites kept the rule: the ANTLR 4 visitor (LUCENE-6417, 5.4.0), the DoubleValues API (LUCENE-7609, 6.4.0), the Java 17 rewrite (#12873, 10.0.0) and the ClassFile API port (#14597, main only); all 113 release tags with the module request an int operand. Not a regression: no release was ever correct. Upstream main 2f424fd428da (2026-10-30) and branch_11x have the same JavascriptCompiler.java as 645d4fe14a20; branch_10_6 matches 10.6.0 and branch_9_12 matches 9.12.3.

Versions and branches executed, with results
Version or commitVerdictResult
4.5.1 no moduleno lucene-expressions artifact exists; the module starts with 4.6.0
initial patch c73f175c71ee (on the 4.6.0 jars) introducing(1 << 32) ? 1 : 0 = 0.0, assertions on and off
4.6.0 to 10.6.0 (122 releases) first: 4.6.0122 of 122 fail (main() port, assertions off); 4.6.0 also with assertions on
10.6.0, 10.5.2, 9.12.3 affectedthe JUnit port fails: expected:<1.0> but was:<0.0>
main 645d4fe14a20 affectedthe one-test repro fails through gradle and the standalone runner, assertions on and off
fix B / fix A on 645d4fe14a20 not affectedfix B: the repro passes (assertions on and off) and 174 of 174 module tests pass; fix A: 174 of 174

Independent checks

An independent reproducer, an intended-behaviour devil's advocate, two executed reviews by another model family (OpenAI GPT-5.6) and an adjudication all rated it real; the adjudication confirmed it with high confidence as a code bug. The one-test repro still fails on main 645d4fe14a20.

Checks and verdicts331 words

Before the finding was recorded, an executed refute review by OpenAI GPT-5.6 reproduced it with its own test (bit 40 of 2^40 + 8) while its low-bit and != 0 controls passed. Its test was re-run on 645d4fe14a20 with the same result.

Each independent check and its verdict
CheckVerdictWhat it found
Finder (a hunt unit) candidate (semi-formal)Own JUnit fails 2 of 5 (assertions on and off); its main() form fails on 10.5.2, 9.12.3 and 8.11.4; two fix sketches pass it and all 170 expressions tests.
Refute review (OpenAI GPT-5.6, executed) real (high)Own test on bit 40 fails 1 of 3 with assertions on and off and two seeds; controls pass. Re-run on 645d4fe14a20: the same.
Independent reproducer realOwn JUnit fails 9 of 10 on main (assertions on and off, second seed) and on 10.5.2, 9.12.3 and 8.11.4: !, &&, ||, ?:, hex, octal, ~ and an indexed field; its premises test passes; its own double-operand patch passes 10 of 10.
Intended lens (OpenAI GPT-5.6, executed) real (high)Own JUnit fails 2 of 6 on main; its main() form fails on 10.5.2; both fixes pass 6 of 6 and all 170 expressions tests. Intended argument rated moderate.
Devil's advocate realInlined and name-bound sub-expressions disagree; (1 << 32) ? 1 : 0 = 0 vs ((1 << 32) + 0) ? 1 : 0 = 1; JavaScript gives 1 for both headline forms (for (1 << 32) because it takes shift counts mod 32). Found that a saturating fix changes long shift counts.
Adjudication confirmed (high), a code bugRe-ran the independent reproduction with a new seed (9 of 10 fail, assertions on and off) and the finder's test (2 fail).
Re-verification on 645d4fe14a20 realThe one-test repro fails on main (assertions on and off), 10.5.2 and 9.12.3; a 64-bit operand guard makes it pass, 0 of 17 probe rows wrong, 170 expressions tests pass.

Diagram file

The diagram as a still image, generated with AI and checked against the code. It is under 10 MB, the limit for a GitHub attachment.

Sources and provenance

Finding record, Lucene and upstream sources, repro files and diagram notes1,513 words

Finding record

LFV-181, snapshot of 2026-11-06

Registry title
Expressions: !, &&, || and ?: keep only the low 32 bits of a bitwise/shift result or hex/octal literal, so a non-zero value such as 1 << 32 or 0x100000000 counts as false
Area
expressions / JavascriptCompiler code generation (boolean operators on long-typed operands: bitwise and shift results, hex and octal literals)
Priority
Low, rank 866: Expression boolean operators keep only the low 32 bits of long operands, so high-bit flag tests silently read false, a specific idiom that a quick test reveals.
Impact class
silent wrong results
Impact note
Anyone using the optional expressions module (default settings, every release since 4.6.0, with -ea and -da, at any index size) who applies !, &&, || or ?: directly to a bitwise or shift result or a hex/octal literal whose value is non-zero but has all-zero low 32 bits, typically a test of flag bit 32..62 of a long doc-values field such as '(flags & 0x100000000) ? 10 : 1' or '!(flags & 0x100000000)', silently gets the false outcome for every document, so scores, sort keys or expression-based filter matches are wrong with no error; flag bits 0..31 and the comparison forms '(e) != 0' and '(e) == 0' are evaluated correctly.
Summary
You compile a Lucene expression that tests a high bit of a long field, such as '(flags & 0x100000000) ? 10 : 1' over a NumericDocValuesField with flags = 2^32 + 1, and expect 10, because 'flags & 0x100000000' evaluates to 4294967296 and '(flags & 0x100000000) != 0' is 1. Instead you get 1: the compiler asks for the operand of !, &&, || and ?: as a 32-bit int and narrows a long-typed operand (the result of & | ^ ~ << >> >>>, or a hex/octal literal) by keeping only its low 32 bits, so every non-zero value whose low 32 bits are zero counts as false. The same number is true when written as the decimal literal 4294967296, as a variable or as '((1 << 32) + 0)', but false as '0x100000000' or '(1 << 32)'.
Introduced
c73f175c71ee (c73f175c71ee02d057819037105b4e55d4099567, 'LUCENE-5207: initial patch from ryan' on the lucene5207 feature branch, svn r1522767, committed by rmuir from a patch by rjernst): the first JavascriptCompiler already compiled the operands of !, &&, || and ?: with expected type INT followed by IFEQ/IFNE, converted long results to int with L2I and pushed int-position hex/octal literals as (int); it reached trunk as eecd78f37a12 (svn r1523462) and branch_4x as 08df432f8ba2 (svn r1523470), both 'LUCENE-5207: lucene expressions module'; issue LUCENE-5207 = apache/lucene#6271, no PR (SVN era) [read: the initial patch in the history clone; no commit-level bisection is possible because no earlier implementation exists] [exec: module boundary, 4.5.1 has no lucene-expressions artifact and 4.6.0 fails]; every later refactor of the code generator kept the rule (ANTLR 4, LUCENE-6417 7c9d78a2c45, 5.4.0; DoubleValues API, LUCENE-7609 8b055382d6c, 6.4.0; Java 17, GITHUB#12873 880d0ba1a8b, 10.0.0; ClassFile API, GITHUB#14597 1b5c25f3d48, main only), which is where plain git blame points [read] (2013-09-13 (feature-branch commit); 2013-09-15 on trunk and branch_4x)
First affected version
4.6.0 [exec], the first release of the expressions module; 4.5.1 and earlier have no expressions module (no lucene-expressions 4.5.1 artifact on Maven Central, executed), so no release was ever unaffected
Affected versions
every lucene-expressions release on Maven Central (121 versions, 4.6.0 through 10.5.2), 10.6.0 [exec: the follow-up's main() port fails on all 122 lucene-expressions releases from 4.6.0 to 10.6.0] and main ea57360912ed. [exec] the follow-up's version probe (6 expressions over flags = 2^32 + 1: 'flags & 0x100000000' = 4294967296 and '(flags & 0x100000000) != 0' = 1 are correct, the ?:, !, && and || forms are wrong) fails 4 of 6 on 4.6.0 (-ea and -da), 4.10.4, 5.5.5, 6.3.0, 6.4.0, 6.6.6, 7.0.0, 7.7.3, 8.0.0, 8.11.4, 9.0.0, 9.12.3, 10.0.0, 10.5.2 (-ea and -da) and main (-da), and on 4.6.0-6.3.0 also shows '0x100000000 ? 1 : 0' = 0 and '(1 << 32) ? 1 : 0' = 0; the independent reproduction's 10-test JUnit fails 9 of 10 on main, 10.5.2, 9.12.3 and 8.11.4. [read] every release tag in the history clone (4.6.0-8.8.1, 9.0.0-10.5.2) requests an int operand for the four boolean operators, converts long results to int and pushes int-position literals as (int); 8.8.2-8.11.3 have no tag in the clone and are inferred from 8.8.1 [read] and 8.11.4 [exec]; branch_10x, branch_10_6 and branch_9_12 hold the same code [read via the upstream contents API, 2026-10-26]. Not a regression; not fixed upstream (JavascriptCompiler.java on upstream main is byte-identical to the tested file). Old releases were run on JDK 25 on aarch64 only (l2i and IFEQ/IFNE are fixed by the JVM specification).
Status
documented (brief, checks and a per-finding doc exist)
Verification
Confirmed by AI-run checks: an independent repro, a cross-model review that agreed, an intended-behaviour check, a prior-report search and a skeptical audit; not yet triaged by a person.
Found by
semi-formal: exhaustive check, code reading; also reached by Z3
Upstream and prior reports
none found (2026-10-26, follow-up prior-report record verdict new: 183 logged queries: a scripted prior-report search with 16 GitHub, 3 commit, 2 downstream, 28 mailing-list (dev@, java-user@, issues@, commits@), 5 JIRA and 6 web queries (301 hits: 0 high, 14 medium, all medium hits read); 75 GitHub GraphQL searches over apache/lucene, the Elasticsearch, OpenSearch, Solr and Lucene.NET repositories and all of GitHub (class and method names, label:module:expressions, bitwise, ternary, truthiness, hex, shift, 64-bit, '0x100000000'); 10 JQL queries including all 11 JIRA issues in component modules/expressions; 27 more list queries; 7 web queries; the body and all comments of 22 apache/lucene issues and PRs, 8 downstream items and 2 Solr JIRA issues read; the changed files of all 413 open apache/lucene PRs): nobody reports or discusses boolean operators on long-valued operands or the low-32-bit narrowing; re-checked 2026-10-26 with gh search for JavascriptCompiler sorted by update (no new item); not fixed: upstream main HEAD is ea57360912ed, branch_10x, branch_10_6 and branch_9_12 have the same code, and no open PR touches JavascriptCompiler.java (#681 and #16847 touch other expressions files); related, not reports of this bug: #3534 (open feature request LUCENE-2460; in 2024 gsmiller recommended expressions for bitwise filtering on numeric doc values, the use this bug breaks for bits 32 and up), elastic/elasticsearch#26272 (a downstream flag test in an expression that failed because of operator precedence, with small values), #6271 (LUCENE-5207, origin; 88 comments, none on boolean operand typing), #7476, #12873 and #14597 (rewrites that kept the behaviour), apache/lucenenet#996 and #1034/#1048 (shift and bitwise fixes in the .NET port, not boolean operators); gaps: GitHub search is lexical, and private reports to security@lucene.apache.org are not visible
Lucene commit tested
main 645d4fe14a20 (commit of 2026-10-28): this doc's one-test repro through gradle in a scratch worktree with assertions on and off (seed DEADBEEF), and on the standalone runner (seed DEADBEEF with assertions on and off, seed 5EED1234 with assertions off); fixes B and A through gradle with the whole lucene/expressions test suite on 2026-10-31. The follow-up's sweep executed a main() port on all 122 lucene-expressions releases from 4.6.0 to 10.6.0 (all fail) and on the initial patch c73f175c71ee; the JUnit port fails on the 10.6.0, 10.5.2 and 9.12.3 jars. The hunt, verifiers and judge ran on main ea57360912ed; nothing in lucene/expressions changed between the two commits. Upstream main 2f424fd428da (2026-10-30) has the same JavascriptCompiler.java.

Lucene and upstream sources

Finding record
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:111 (@lucene.experimental), :513-526 (visitUnary BOOLNOT: typeStack.push(TypeKind.INT) at :518, ifeq at :521), :528-534 (~: lxor, then gen.conversion(TypeKind.LONG, typeStack.peek()) at :534), :690-705 (visitBooland: INT at :694, ifeq), :710-725 (visitBoolor: INT at :714, ifne), :730-737 (visitConditional: INT at :734, ifeq), :755-774 (pushBitwise / pushBinaryOp: gen.conversion(returnType, typeStack.peek()) at :773 = l2i in an INT context), :793-797 (pushLong: gen.loadConstant((int) i) at :796 in an INT context)
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/package-info.java:21-22 ('an expression syntax that's based on JavaScript expressions'), :25 (hex and octal literals), :27 (bitwise operators), :28 (boolean operators && || ! ?:), :38-41 (JavaScript precedence; 'b is only evaluated if a is not true')
  • lucene/expressions/src/test/org/apache/lucene/expressions/js/TestJavascriptOperations.java:183 ('!2' = 0), :214 ('-1 ? 2 : 3' = 2), :238 ('-15 << 62' = 4611686018427387904), :262 ('-5 >>> 33' = 2147483647), :331 ('0x1234ABCDEF' = 78193085935)
  • read-only upstream check 2026-10-26: gh search issues --repo apache/lucene JavascriptCompiler sorted by update (newest item #3534, 2024-08-29)
The finding in 30 seconds
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:432-439, (at 645d4fe14a20) :494, :503, :513-534, :646-666 (pushCond, typeStack.push(DOUBLE) at :650), :690-745, :747-774, :793-808
  • lucene/expressions/src/test/org/apache/lucene/expressions/js/TestJavascriptOperations.java:183, (at 645d4fe14a20) :214, :238, :331
Who is affected
  • lucene/expressions/src/java/org/apache/lucene/expressions/Expression.java:30-66, (at 645d4fe14a20) :93
Why it looks unintended
  • lucene/expressions/src/test/org/apache/lucene/expressions/js/TestJavascriptOperations.java:178-184, (at 645d4fe14a20) :214, :238, :260, :331
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/package-info.java:21-28; (at 645d4fe14a20) JavascriptCompiler.java:111
Suggested fix
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:513-526, (at 645d4fe14a20) :690-745, :776-791
Diagrams
  • lucene/expressions/src/java/org/apache/lucene/expressions/js/JavascriptCompiler.java:734-737, (at 645d4fe14a20) :773, :796

Other statements trace to the hunt's working files (models, logs, reviews and briefs), which are not published.

Repro files

About the diagrams

A picture of the minimal repro and the 64-to-32-bit narrowing, made in parallel with this doc from the code at 645d4fe14a20; the PNG is the summary still.

Built 2026-11-06 from the hunt's record of this finding and its doc file. Lucene main under test in this hunt: ea57360912ed. Every listed finding was re-run on Lucene main e1faeba08bde on 2026-11-02.