ffi 2.16b green: lexical-direct env in #jni_call inside #jni_env

`Lowering` gains a `jni_env_stack: ArrayList(Ref)`. When lowering
the `jni_env_block` arm pushes the env_expr's Ref before lowering
the body and pops after; `defer` ensures cleanup on early return.

`lowerJniCall` now disambiguates explicit-vs-omitted env via the
position of the first string-literal arg: at index 1 → omitted
(3-arg form `target, "name", "sig"`), at index 2 → explicit
(4-arg form `env, target, "name", "sig"`). Omitted form reads the
top of `jni_env_stack`; missing scope → diagnostic.

End-to-end test runs cleanly. Locked-in IR snapshot at
`tests/expected/ffi-jni-env-02-lexical-direct.ir` shows env coming
from the enclosing fn's `*void` param straight into the jni_msg_send
expansion — no extra load, no thread-local read. The hot-path
optimisation from the design discussion is now real.

128/128 examples + 1 new IR snapshot green; zig test clean.
This commit is contained in:
agra
2026-05-20 10:54:37 +03:00
parent e463385404
commit 022ca31050
4 changed files with 330 additions and 13 deletions

View File

@@ -96,6 +96,7 @@ pub const Lowering = struct {
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback)
jni_env_stack: std.ArrayList(Ref) = std.ArrayList(Ref).empty, // lexical `#jni_env(env)` Ref stack — top is current scope's env for omitted-env `#jni_call`
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
@@ -1051,10 +1052,14 @@ pub const Lowering = struct {
.insert_expr => |ins| self.lowerInsertExpr(ins.expr),
.block => self.lowerBlock(node),
.jni_env_block => |eb| {
// 2.16a: evaluate env for side effects, lower body as a normal block.
// TL push/pop semantics land in 2.16b; until then `#jni_env` is a
// syntactic marker that doesn't affect codegen.
_ = self.lowerExpr(eb.env);
// Lexical-direct env resolution (2.16b): evaluate env once,
// push onto the env stack, lower body, pop. `#jni_call`
// sites inside `eb.body` with an omitted env arg pick up
// the top-of-stack value directly — no thread-local read,
// env stays register-resident across the body.
const env_ref = self.lowerExpr(eb.env);
self.jni_env_stack.append(self.alloc, env_ref) catch unreachable;
defer _ = self.jni_env_stack.pop();
self.lowerBlock(eb.body);
},
// Block-local type declarations
@@ -3860,18 +3865,39 @@ pub const Lowering = struct {
}
fn lowerJniCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref {
if (fic.args.len < 4) {
// env disambiguation: the method-name slot is always a string literal,
// so its position tells us whether env was omitted.
// omitted → args = target, "name", "sig", method-args... (≥3)
// explicit → args = env, target, "name", "sig", method-args... (≥4)
const env_omitted = fic.args.len >= 3 and fic.args[1].data == .string_literal;
const min_arity: usize = if (env_omitted) 3 else 4;
if (fic.args.len < min_arity) {
if (self.diagnostics) |d| {
d.add(.err, "#jni_call requires env, target, method name, and signature", null);
d.add(.err, "#jni_call requires env (optional in #jni_env scope), target, method name, and signature", null);
}
return Ref.none;
}
const ret_ty = self.resolveType(fic.return_type);
const env_ref = self.lowerExpr(fic.args[0]);
const target_ref = self.lowerExpr(fic.args[1]);
const name_node = fic.args[2];
const sig_node = fic.args[3];
const env_ref = if (env_omitted) blk: {
if (self.jni_env_stack.items.len == 0) {
if (self.diagnostics) |d| {
d.add(.err, "#jni_call with omitted env requires an enclosing #jni_env scope", null);
}
return Ref.none;
}
break :blk self.jni_env_stack.items[self.jni_env_stack.items.len - 1];
} else self.lowerExpr(fic.args[0]);
const target_idx: usize = if (env_omitted) 0 else 1;
const name_idx: usize = target_idx + 1;
const sig_idx: usize = target_idx + 2;
const first_method_arg_idx: usize = target_idx + 3;
const target_ref = self.lowerExpr(fic.args[target_idx]);
const name_node = fic.args[name_idx];
const sig_node = fic.args[sig_idx];
const name_ref = self.lowerExpr(name_node);
const sig_ref = self.lowerExpr(sig_node);
@@ -3887,7 +3913,7 @@ pub const Lowering = struct {
null;
var extra = std.ArrayList(Ref).empty;
var ai: usize = 4;
var ai: usize = first_method_arg_idx;
while (ai < fic.args.len) : (ai += 1) {
extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable;
}