Skip to content

Call scalar functions by name through the session - #10763

Open
robert3005 wants to merge 3 commits into
developfrom
rk/session-call
Open

robert3005 wants to merge 3 commits into
developfrom
rk/session-call

Conversation

@robert3005

Copy link
Copy Markdown
Contributor

ScalarFnPlugin gains bind_any, which builds the function from options passed as &dyn Any, downcasting them to the function's own ScalarFnVTable::Options type and failing with an error naming that type otherwise.

ScalarFnSessionExt uses it to look a function up in the session's scalar function registry by name and construct the call:

  • scalar_fn(name, options) builds the registered function
  • call(name, options, args) applies it to args, which are arrays (giving the lazy ScalarFnArray) or expressions (giving the expression), through the new ScalarFnArg trait

`ScalarFnPlugin` gains `bind_any`, which builds the function from options passed
as `&dyn Any`, downcasting them to the function's own `ScalarFnVTable::Options`
type and failing with an error naming that type otherwise.

`ScalarFnSessionExt` uses it to look a function up in the session's scalar
function registry by name and construct the call:

- `scalar_fn(name, options)` builds the registered function
- `call(name, options, args)` applies it to `args`, which are arrays (giving
  the lazy `ScalarFnArray`) or expressions (giving the expression), through the
  new `ScalarFnArg` trait

A function without options takes `&EmptyOptions`. This lets code call functions
that another crate registers with the session without depending on that crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ps4mp7KF5c4p66aoSoXx5j
Signed-off-by: Claude <noreply@anthropic.com>
@robert3005 robert3005 added the changelog/feature A new feature label Oct 23, 2026
@codspeed

codspeed Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 20.31%

4 warnings, 1 note

⚠️ Walltime results may be unreliable

Some benchmarks used the Walltime instrument outside CodSpeed Macro Runners, which can make the results inconsistent.

Learn more about Macro Runners

See affected benchmarks

⚠️ Unable to measure the execution of 1 benchmark

This benchmark measured no execution time, so its result is not comparable and counts as unchanged. This usually happens when the compiler removes the code under test.

Prevent compiler optimizations

See affected benchmarks

⚠️ 24 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

ℹ️ 40 benchmarks have no flamegraph

The flamegraph wasn't generated: the symbols of the profiling trace could not be resolved, because no information about the language frames was collected.

See affected benchmarks

⚡ 2 improved benchmarks
❌ 37 regressed benchmarks
✅ 2369 untouched benchmarks
⏩ 12 skipped benchmarks1
🗄️ 43 archived benchmarks run2

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ CPU Simulation pack_return_dtype[100] 166.4 µs 398.2 µs -58.21%
❌ CPU Simulation pack_return_dtype[350] 553.3 µs 1,085.2 µs -49.01%
❌ CPU Simulation pack_return_dtype[500] 764.5 µs 1,493.2 µs -48.8%
❌ CPU Simulation pack_return_dtype[200] 313.8 µs 594.6 µs -47.23%
❌ CPU Simulation bench_compare_sliced_dict_varbinview[(1000, 10000)] 96.3 µs 180.3 µs -46.6%
❌ CPU Simulation listview_length_medium 190.5 µs 276.5 µs -31.09%
❌ CPU Simulation case_when_nary_equality_lookup[1000] 296.2 µs 423.5 µs -30.06%
❌ Walltime threshold_neon[80] 3.5 µs 4.9 µs -28.55%
❌ Walltime filter_neon[8] 893 ns 1,203 ns -25.77%
❌ CPU Simulation bench_compare_primitive[(10000, 32)] 72.9 µs 96.5 µs -24.54%
❌ CPU Simulation case_when_nary_10_conditions[1000] 369 µs 471.2 µs -21.68%
❌ CPU Simulation list_sum_nullable_elements_medium 296.3 µs 377.1 µs -21.44%
❌ CPU Simulation case_when_nary_10_conditions[10000] 446.6 µs 546.1 µs -18.21%
❌ CPU Simulation like_substr_high_match 416.8 µs 506.1 µs -17.65%
❌ CPU Simulation case_when_all_false[1000] 106 µs 127.4 µs -16.81%
❌ CPU Simulation case_when_all_true[1000] 106.5 µs 128 µs -16.81%
❌ Walltime filter_neon[64] 1.7 µs 2.1 µs -16.51%
❌ CPU Simulation case_when_nary_3_conditions[1000] 203.4 µs 243.3 µs -16.4%
❌ CPU Simulation case_when_nary_early_dominant[1000] 201.8 µs 240.7 µs -16.14%
❌ CPU Simulation case_when_simple[1000] 125.7 µs 148 µs -15.05%
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing rk/session-call (1271715) with develop (91b38f4)

Open in CodSpeed

Footnotes

  1. 12 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. 43 benchmarks were run, but are now archived. If they were deleted in another branch, consider rebasing to remove them from the report. Instead if they were added back, click here to restore them. ↩

`lazy(name, options, children)` builds an expression node that holds only the
function id and its options, so it needs neither a session nor the crate that
defines the function. `Expression::resolve(session)` later replaces each such
node with the function registered under that id, built from the options through
`ScalarFnPlugin::bind_any`.

A lazy node is a scalar function vtable, `LazyScalarFn`, like the foreign
placeholder: it reports the target function's id so it displays and hashes like
that function, accepts any arity, and fails to bind, execute, or serialize with
an error asking for resolution. `LazyScalarFnOptions` type-erases the options
behind a trait dispatching equality and hashing by concrete type, so any
`ScalarFnVTable::Options` type fits.

Unknown names, options of the wrong type, and arity mismatches are all reported
by `resolve`. `ScalarFnSessionExt::scalar_fn_by_id` looks a function up by an
already interned id; `scalar_fn(name, ...)` delegates to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EAanvjKHbMGyQyN2nQAwU
Signed-off-by: Claude <noreply@anthropic.com>
An `Expression::Scalar` node no longer holds a `ScalarFnRef`. It holds the id
of the scalar function it calls, the function's options with their type erased
(`ErasedOptions`), and its children. Building an expression therefore needs
neither a session nor the crate that defines the function. `Expression::new`
builds a node from an id and erased options; the vtable constructors
(`new_expr`, `try_new_expr`) still check the arity up front.

Binding resolves the function: `Expression::bind(dtype, session)` and
`Scope`, which now carries the session, look each node's id up in the
session's scalar function registry, build the function from the options
through `ScalarFnPlugin::bind_any`, and check the options type and the number
of children, producing a `BoundExpression` as before. Options read for an id
the session did not register bind to the foreign placeholder.

Consequences across the API:

- `ScalarFnVTable::bind_options` takes `Option<&VortexSession>`: `Some` when an
  expression binds, so options that hold an expression, such as a lambda, can
  bind it; `None` when a bound node is rebuilt or a `ScalarFnArray` is built.
  `Lambda::bind` takes the session, and `Lambda::return_dtype` and
  `Lambda::is_infallible` are gone, since neither can be answered unbound.
- `Expression::return_dtype` is gone; bind and read the dtype instead.
- `ExprSerializeProtoExt::serialize_proto` takes the session, which serializes
  each function's options through the new `ScalarFnPlugin::serialize_any`.
- `ArrayRef::apply` takes the session and binds first.
  `vx_array_apply` and the C++ `Array::apply` take a session too.
- `Expression::is`, `as_opt`, `as_` and `contains` need `V: Default` to know
  the vtable's id, so every unit vtable derives `Default`.
- Unbound expressions display generically, as `id(child, ..., opts=options)`,
  with numbered children in `display_tree`; binding gives each function its
  own formatting. Tests that assert on display bind first.
- `DynamicComparison` and `RowCount` are registered by default, and the new
  `vortex_layout::initialize` registers `RowIdx`, since binding now needs
  every function an expression calls to be registered.

`ScalarFnSessionExt` gains `scalar_fn_by_id`, `scalar_fn_plugin` and
`resolve_scalar_fn`. The lazy expression modules from the previous commit are
removed, since every expression is now built this way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EAanvjKHbMGyQyN2nQAwU
Signed-off-by: Claude <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
docs-preview/pr-10763 — 1271715c Deployed Oct 24, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants