Skip to content

Add standalone JIT expression compiler [DO NOT MERGE] - #3047

Draft
fulmicoton-dd wants to merge 6 commits into
mainfrom
paul.masurel/jitexpr-core
Draft

Add standalone JIT expression compiler [DO NOT MERGE]#3047
fulmicoton-dd wants to merge 6 commits into
mainfrom
paul.masurel/jitexpr-core

Conversation

@fulmicoton-dd

@fulmicoton-dd fulmicoton-dd commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

JitExpr is an expression JIT compiler for tantivy.
The goal here is to be able to evaluate dynamically defined predicate over columns.

The compilation goes through different representations:

  UntypedExpr
      ↓ injecting variable types, and type checking
  TypedExpr
      ↓ lowering
  Cranelift IR
      ↓ Cranelift code generation
  Machine code (or assembly)

Function specific logic is isolated in a FnCall trait that offers rails for AI / humans to extend the system
safely and in a uniform manner.

Input and output variable are 16 bytes VariableValue object that are Copy.
Primitive types are using 8 bytes for the value and 8bytes for the bool (alignement)

String are also supposed as Option<&str> and also take 16 bytes.
strings are not manipulated by the cranelift program itself. Instead the program calls native function and
treats them as opaque objects.

Function that return reference to an input string or a literal just return a reference over one or the other.
Function that build string use a string arena as their scratch pad to write their bytes and return a &str
pointing to it.

As a result the return type has a lifetime limited to &self (literal + string arena) and the input lifetime (regex extract for instance).

Use of AI

Most of the function themselves (all but ADD, REGEXP_EXTRACT, LOWER), the unit tests, the serialization format code is isolated and has been coded by LLM with very little review.

@fulmicoton
fulmicoton requested a review from Mallets August 19, 2026 10:07
@fulmicoton fulmicoton changed the title Add standalone JIT expression compiler Add standalone JIT expression compiler [DO NOT MERGE] Aug 19, 2026
// SAFETY: Same input contract as above. The fixed arena never reallocates,
// and its new output range starts after any arena-backed input range.
let input =
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(input_ptr, input_len)) };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this code is really dumb...

@fulmicoton-dd
fulmicoton-dd force-pushed the paul.masurel/jitexpr-core branch from aba7127 to 4910fad Compare August 19, 2026 21:10
Comment thread jitexpr/examples/basic.rs
Comment thread jitexpr/src/compile/mod.rs
Comment thread jitexpr/examples/basic.rs
let variable_types: HashMap<&str, VarType> =
std::iter::once(("my_col", VarType::F64)).collect();

let compiled_fn: Arc<CompiledFn> = compile(&untyped_expr, &variable_types)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm missing the point of returning separately a CompiledFnCtx and CompiledFn. In this example the actual .call happens on compiled_fn_ctx and not on compiled_fn. Can CompiledFn be used without being passed to CompiledFnCtx?

@fulmicoton fulmicoton Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Make function compilation cachable.
It takes a few ms to compile but generally the compilation applies to all splits. (rather we can generate/cache one version per set of column types)

Comment thread jitexpr/src/ast/literal.rs Outdated
Literal::I64(value) => InferredTypeSet {
i64: true,
u64: *value >= 0,
f64: (*value as f64) as i128 == *value as i128,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't really understand this magic of casting a f64 to i128 :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I will try to add comments and wrangel the code into something that makes sense.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

with furhter thoughts we don't want that behavior and we want to always accept f64.

Explaining the presence of i128 though.

The question this was trying to answer is given a u64, when can it be translated into f64 in a loss less manner.

By lossless, we do not just mean:
u64 -> f64 -> u64 brings us back to the same value,
we also want to make sure that the f64 value was indeed representing the original u64 value so that
we do not have strange side effect if we did arithmetic in the f64 world.

u64::MAX does NOT map to its value in f64 world, but remaps correctly to u64::MAX due to saturation.
Using u128 make it possible to rule out u64::MAX as having a f64 representation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation!

Comment thread jitexpr/src/ast/infer_types.rs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This file is used only in bin/jitexpr-asm, it's not used anywhere else to parse user input.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

good point i will try to isolate it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

After investigation, the benefit might not be as large as the gain.
(either via feature flag or via creation of a project).

Let's do the separate project if we end up adding some binary specific dependency. Right now this is not the case.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(right now it has the benefit of offering a good looking Debug/Display. Deserialization is pretty useless though)

)))]
compile_error!(
"the direct VariableValue JIT return ABI is only implemented for x86-64 System V and AArch64"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This #[cfg] does not exclude aarch64-pc-windows-*, I didn't test but I'm expecting it to fail as well.

@fulmicoton fulmicoton Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yes it is not supported because the ABI does not support returning more than a single word via register. I commented along these lines. If we want to support windows the natural fix would be to change that to passing a destination value pointer as arg.

@fulmicoton-dd
fulmicoton-dd force-pushed the paul.masurel/jitexpr-core branch from 71cd5a3 to 0f3f25f Compare August 31, 2026 15:59
@fulmicoton-dd
fulmicoton-dd force-pushed the paul.masurel/jitexpr-core branch from 0f3f25f to 10aa21a Compare August 31, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants