Skip to content

feat: add on_missing_method callback hook (internals)#1067

Merged
schungx merged 9 commits intorhaiscript:mainfrom
yuvalrakavy:on-missing-method
Mar 24, 2026
Merged

feat: add on_missing_method callback hook (internals)#1067
schungx merged 9 commits intorhaiscript:mainfrom
yuvalrakavy:on-missing-method

Conversation

@yuvalrakavy
Copy link
Contributor

Summary

Add an on_missing_method callback to the Engine, gated behind the internals feature. When a method call fails to resolve to any registered function, this callback is invoked before raising ErrorFunctionNotFound, giving embedders a chance to handle the call dynamically.

This follows the same pattern as the existing on_missing_map_property callback.

Motivation

When embedding Rhai in systems with dynamic method dispatch (e.g., object-oriented stores where methods are defined at runtime), there's currently no way to intercept failed method calls. The only option is to register every possible method upfront, which isn't feasible when methods are user-defined and change at runtime.

With on_missing_method, the host application can:

  • Walk class hierarchies to find inherited functions
  • Dispatch to externally-defined methods
  • Implement prototype-chain-style method resolution

API

engine.on_missing_method(|name: &str, args: &mut [&mut Dynamic], ctx: EvalContext| {
    // args[0] is the object the method was called on
    // Return Ok(Some(value)) to provide a result
    // Return Ok(None) to fall through to ErrorFunctionNotFound
    // Return Err(e) to propagate an error
    Ok(None)
});

Changes

  • src/api/events.rsEngine::on_missing_method() registration method
  • src/engine.rsmissing_method field on Engine
  • src/func/native.rsOnMissingMethodCallback type alias (sync/non-sync variants)
  • src/func/call.rs — Invoke callback before ErrorFunctionNotFound in method dispatch
  • tests/on_missing_method.rs — 7 tests covering basic usage, fallthrough, argument passing, existing method priority, error propagation, custom types, and multiple arities

Test plan

All 7 new tests pass. Full test suite passes with --features internals.

Add Engine::on_missing_method() which is called when a method call
fails to resolve. The callback can return Ok(Some(value)) to provide
a result, Ok(None) to fall through to ErrorFunctionNotFound, or
Err() to propagate a custom error.

Modeled after on_map_missing_property and on_invalid_array_index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@schungx
Copy link
Collaborator

schungx commented Mar 7, 2026

Great addition. However, it seems to work on all functions, not just method calls. So perhaps on_missing_function?

In which case, you'd need parameters in the callback to distinguish between two cases:

  1. method call vs. normal function call (arg[0] is the object or not, usually called is_method in the codebase)
  2. is the first argument &mut or a clone (usually called is_ref_mut in the codebase)

And you have to search for all the places where function resolution is performed (via the resolve function) and make sure that you handle each case. Should be only a few places.

@schungx
Copy link
Collaborator

schungx commented Mar 21, 2026

Just checking on the status of this PR. There are a number of CI failing also.

And pls check my review items. Thanks!

yuvalrakavy and others added 2 commits March 22, 2026 13:52
…hod_call param

- Rename callback from on_missing_method to on_missing_function since it
  handles both method-style and function-style calls
- Move hook from exec_native_fn_call up to exec_fn_call where scope and
  is_method_call flag are available
- Add is_method_call: bool parameter to the callback so callers can
  distinguish method calls (obj.method()) from function calls (func())
- Pass real scope instead of Scope::new()
- Align doc comments with on_missing_map_property style
- Document that qualified calls (module::function()) are not covered,
  leaving room for a future on_missing_qualified_function if needed
- Add #[deprecated] volatile marker consistent with other internals APIs
- Add test for is_method_call flag

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yuvalrakavy
Copy link
Contributor Author

Hi @schungx, thanks for the review and sorry for the delay. I've addressed all your feedback:

Renamed to on_missing_function — you were right that the callback fires for both method-style and function-style calls, so on_missing_function is more accurate.

Added is_method_call: bool parameter — rather than restricting the callback to methods only, I pass the flag through so callers can decide what to handle. The signature is now:

Fn(&str, &mut [&mut Dynamic], bool, EvalContext) -> RhaiResultOf<Option<Dynamic>>

Moved the hook from exec_native_fn_call to exec_fn_call — this is where both scope and is_method_call are available. The callback now receives the real calling scope instead of an empty Scope::new().

Qualified calls (module::function()) are intentionally not covered — intercepting explicitly namespaced calls would undermine the purpose of qualification. If there's demand in the future, a separate on_missing_qualified_function with a namespace parameter would be cleaner than overloading this callback.

Doc comments now follow the on_missing_map_property style, with the #[deprecated] volatile marker.

Tests renamed to on_missing_function.rs with updated signatures and a new test for the is_method_call flag.

The CI runs need your approval to start (fork contributor restriction). Could you approve them? Happy to fix anything they flag.

@schungx
Copy link
Collaborator

schungx commented Mar 22, 2026

BTW the new tests... do they need to be in a new file? Or can you just put them under test_functions.rs or something...

@schungx
Copy link
Collaborator

schungx commented Mar 22, 2026

Some CI have failed... You may also need to do a cargo fmt to reformat the files...

yuvalrakavy and others added 2 commits March 22, 2026 16:46
- Add #[cfg(not(feature = "no_object"))] to all tests using dot notation
  since method-call syntax is unavailable with no_object
- Use rhai::INT instead of i64 for only_i32 compatibility
- Use INT in register_fn signatures to match the engine's integer type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yuvalrakavy
Copy link
Contributor Author

CI fixes pushed:

  • no_object: gated all method-call tests with #[cfg(not(feature = "no_object"))] since dot notation is unavailable with that feature
  • only_i32: switched tests from i64 to rhai::INT so they work with both i32 and i64 integer types
  • Formatting: ran cargo fmt

Should be ready for another CI run when you get a chance to approve. Thanks!

@schungx
Copy link
Collaborator

schungx commented Mar 22, 2026

Final question: are you sure this needs to require internals?

on_missing_map_property is also internals because it returns Target.

This one doesn't involve any internals data structure and can probably be available generally... Although it might still be internals for consistency...

@schungx
Copy link
Collaborator

schungx commented Mar 23, 2026

There still seems to be failed tests under no_object.

Incidentally, we'd need to decide whether to omit the is_method_call parameter under no_object... Since I suppose that parameter would always be false under no_object?

Move all on_missing_function callback tests from the separate
on_missing_function.rs file into the existing functions.rs test file,
as suggested in PR review. Each test is gated with
#[cfg(feature = "internals")] and #[cfg(not(feature = "no_object"))].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@yuvalrakavy
Copy link
Contributor Author

Thanks @schungx, I've addressed all three points:

Tests moved — done in the latest commit (8db04c9). All on_missing_function tests are now in tests/functions.rs, each gated with #[cfg(feature = "internals")] and #[cfg(not(feature = "no_object"))]. This should fix the no_object CI failure.

internals gate — I agree it could be made generally available since the callback signature only uses public types (&str, &mut [&mut Dynamic], bool, EvalContext). However, EvalContext is itself gated behind internals. So removing the gate from on_missing_function would also require exposing EvalContext without internals — which is a bigger decision. Happy to go either way; keeping internals for consistency with on_missing_map_property seems safe for now, and it can always be relaxed later.

is_method_call under no_object — I'd recommend keeping the parameter in the signature regardless of feature flags. It simplifies the API (one consistent callback signature), and under no_object it just always passes false. Conditional compilation on callback signatures would make the API harder to use and document. The cost is a single bool that's always false — negligible compared to the API complexity of two different signatures.

@schungx
Copy link
Collaborator

schungx commented Mar 23, 2026

Agree on all three counts. Let's leave it as it is.

But shouldn't there also be tests even under no_object that calls a missing function?

@schungx
Copy link
Collaborator

schungx commented Mar 23, 2026

Other than the formatting, it is failing on the example in the doc comments:

let result = engine.eval::<String>(r#"let x = 42; x.greet()"#)?;

Need to gate it under not(feature = "no_object").

Or put in two versions:

/// # #[cfg(not(feature = "no_object"))]
/// let result = engine.eval::<String>(r#"let x = 42; x.greet()"#)?;
/// # #[cfg(eature = "no_object")]
/// let result = engine.eval::<String>(r#"let x = 42; greet(x)"#)?;

yuvalrakavy and others added 2 commits March 23, 2026 17:02
The doc example for on_missing_function uses x.greet() which requires
dot notation unavailable under no_object. Add cfg gates to provide
alternative function-style call for no_object builds. Also add
#[allow(deprecated)] since the function carries a volatile API marker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@schungx
Copy link
Collaborator

schungx commented Mar 24, 2026

I have a hunch that it may fail because it is not a method call:

let result = engine.eval::<String>(r#"greet(42)"#)?;

EDIT: Yup, it does.

I suggest putting the #[cfg(not(feature = "no_object"))] gate on the entire code block to skip it under no_object.

Or you'd need to do:

if (cfg!("feature = "no_object") || is_method_call) && name == "greet" {

@schungx
Copy link
Collaborator

schungx commented Mar 24, 2026

Yay! Merge!

@schungx schungx merged commit 945d610 into rhaiscript:main Mar 24, 2026
42 checks passed
@yuvalrakavy
Copy link
Contributor Author

Thanks Stephen! Really appreciate the thorough review and the quick merge.

Rhai is a very important cornerstone in an object store database I'm building, so having on_missing_function in the standard distribution means I can move back to the official releases and stop relying on a private fork. Great to have this upstream!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FR: "Function not found" error should spec desired arity, and perhaps available arities

2 participants