Skip to content

Closures keep the imports of the script that made them (fixes #1193) - #1194

Merged
schungx merged 3 commits into
rhaiscript:mainfrom
luispedro:fix-1193-closure-modules
Oct 23, 2026
Merged

schungx merged 3 commits into
rhaiscript:mainfrom
luispedro:fix-1193-closure-modules

Conversation

@luispedro

Copy link
Copy Markdown
Contributor

Fixes #1193.

Problem

Since 648c684 ("Remove unnecessary embedded environment from FnPtr"), a closure gets its environment only from its function's entry in the lib, which closures of the main script don't have. A closure kept by the host and called after the script has finished, with FnPtr::call, no longer finds the script's imports:

import "m" as m; keep(|| m::who());

fails with ErrorModuleNotFound("m") when the kept closure is called. It worked in 1.26.1. The bisect and the repro are in the issue.

Changes

Commit 1: AST walker. Put back FnPtr::env, set when a closure literal is evaluated (as before 648c684), and prefer it to the lib's environment when calling. The bare function name as a variable (6f4ed21) keeps env: None, so the fix for a module's closure seeing the caller's functions stays. FnPtr is 48 bytes again. Test: test_module_closure_keeps_imports.

Commit 2: Grain. The first commit left Grain unchanged, and it hit the same error: the compiler pools closure literals as constants and the VM pushed them with env: None.

  • The VM now gives a loaded FnPtr constant an environment built from its global state, as get_literal_value does for the walker. It checks only when the program uses function pointers (Caps::FN_PTR), so other programs pay one flag test per constant load.
  • constant_caps refuses a FnPtr that already has an environment, so such a constant is not pooled. The compiler leaves it to the walker instead of writing an artifact that loses the environment and fails the same way when read back. (As suggested in the issue discussion.) I could not get an env: Some constant into an AST through the public API, since the optimizer rebuilds scope-constant closures as Fn("anon$…") calls, so this guard is defensive.
  • Tests: a Grain version of the repro (a_kept_closure_keeps_the_imports_of_its_script) and a unit test for constant_caps.

Design note

I did not fragment every closure literal into a residual so that the walker sets the environment. That would stop a.map(|x| ..) and similar from lowering, and the Grain AGENTS.md says to prefer instructions over fragments. Setting the environment in the VM needs no new opcode and no wire-format change.

Testing

cargo test --tests passes with the default features, internals,grain, bin-features,grain,internals, and internals,grain combined with each of no_closure, no_function, no_module and no_object,no_index. The library builds without warnings under several no_* combinations.

🤖 Generated with Claude Code

luispedro and others added 2 commits October 23, 2026 11:58
Since 648c684 ("Remove unnecessary embedded environment from FnPtr"), a
closure gets an environment only from its function's entry in the lib,
which closures of the main script don't have. Called after the script
has finished (a callback that the host keeps and calls later with
FnPtr::call), such a closure no longer finds the script's imports:

    import "m" as m; keep(|| m::who());

fails with ErrorModuleNotFound("m") when the kept closure is called. It
worked in 1.26.1.

Put back FnPtr::env, set when a closure literal is evaluated as before,
and prefer it to the lib's environment when calling. The bare function
name as a variable (6f4ed21) keeps env None, so the fix for a module's
closure seeing the caller's functions stays. FnPtr is 48 bytes again.

Rhai Grain is unchanged: it pools closure literals as constants and never
fills the environment, so it still fails on this script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Grain compiler pools closure literals as constants, and the VM pushed
them with env None, so a closure kept by the host and called later with
FnPtr::call still failed with ErrorModuleNotFound on the Grain path.

Load a FnPtr constant with an environment taken from the VM's global
state, as get_literal_value does for the walker. Only programs that use
function pointers (Caps::FN_PTR) pay for the check.

Also refuse to pool a FnPtr constant that already has an environment, so
the compiler leaves it to the walker instead of writing an artifact that
loses the environment and fails the same way when read back.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@schungx schungx added the bug label Oct 23, 2026
@schungx

schungx commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

There is a test that fails. Probably should have used the Locked and Shared wrappers since under sync feature they map to different types.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@schungx
schungx merged commit 1817eb5 into rhaiscript:main Oct 23, 2026
56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1.27.0: a closure loses the script's imports when called after the script has run

2 participants