ffi 2.16a green: parser + AST + sema for #jni_env(env) { body }

New `hash_jni_env` lexer token; `parsePrimary` dispatches to a small
`parseJniEnvBlock` that consumes `(env) { body }` and returns a new
`JniEnvBlock` AST node (env_expr + body block).

Sema's analyzeNode arm recurses into env + body inside a pushed
scope; findNodeAtOffset descends through both children for go-to-
definition.

Lowering treats it as a syntactic wrapper around the block: env is
evaluated for side effects, body lowers as a normal block. The TL
push/pop semantics (synthesizing the env stack so `#jni_call`'s env
arg can become optional) land in 2.16b.

`expectSemicolonAfter` recognises `jni_env_block` as block-form so
statement-position uses don't need a trailing `;` — matches `if` /
`while` / `for` / bare blocks.

Test runs through the block body and prints expected output; xfail
snapshot flips to green. 127/127 examples green.
This commit is contained in:
agra
2026-05-20 10:41:24 +03:00
parent 93adde5a3d
commit 5bd2c84bb6
9 changed files with 53 additions and 2 deletions

View File

@@ -82,6 +82,7 @@ pub const Node = struct {
impl_block: ImplBlock,
ffi_intrinsic_call: FfiIntrinsicCall,
foreign_class_decl: ForeignClassDecl,
jni_env_block: JniEnvBlock,
pub fn declName(self: Data) ?[]const u8 {
return switch (self) {
@@ -571,6 +572,11 @@ pub const ForeignClassDecl = struct {
members: []const ForeignClassMember = &.{},
};
pub const JniEnvBlock = struct {
env: *Node, // expression yielding the *JNIEnv for this scope
body: *Node, // block (or expression) — runs with `env` scoped via TL push/pop
};
pub const ImplBlock = struct {
protocol_name: []const u8,
target_type: []const u8,

View File

@@ -1050,6 +1050,13 @@ pub const Lowering = struct {
.destructure_decl => |dd| self.lowerDestructureDecl(&dd),
.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);
self.lowerBlock(eb.body);
},
// Block-local type declarations
.struct_decl => |sd| self.registerStructDecl(&sd),
.enum_decl, .union_decl => {

View File

@@ -92,6 +92,7 @@ pub const Lexer = struct {
.{ "#extends", Tag.hash_extends },
.{ "#implements", Tag.hash_implements },
.{ "#jni_method_descriptor", Tag.hash_jni_method_descriptor },
.{ "#jni_env", Tag.hash_jni_env },
};
inline for (directives) |d| {
const keyword = d[0];

View File

@@ -1508,6 +1508,7 @@ pub const Server = struct {
.hash_extends,
.hash_implements,
.hash_jni_method_descriptor,
.hash_jni_env,
=> ST.keyword,
.kw_f32, .kw_f64, .kw_Type, .kw_Self => ST.type_,

View File

@@ -1558,6 +1558,7 @@ pub const Parser = struct {
.while_expr => false,
.for_expr => false,
.block => false,
.jni_env_block => false,
else => true,
};
if (needs_semi) {
@@ -2187,6 +2188,9 @@ pub const Parser = struct {
.hash_objc_call, .hash_jni_call, .hash_jni_static_call => {
return try self.parseFfiIntrinsicCall();
},
.hash_jni_env => {
return try self.parseJniEnvBlock();
},
else => {
return self.fail("unexpected token in expression");
},
@@ -2198,6 +2202,27 @@ pub const Parser = struct {
/// `#jni_static_call(T)(class, "name", "(Sig)R", args...)`. The
/// return type sits in the first parens; the actual call args
/// follow in the second.
fn parseJniEnvBlock(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
self.advance(); // skip `#jni_env`
try self.expect(.l_paren);
const env_expr = try self.parseExpr();
try self.expect(.r_paren);
// Body is a brace-delimited block. The `-> ?T` annotation for
// exception bubbling lands with step 2.15 / 2.16 follow-ups.
if (self.current.tag != .l_brace) {
return self.fail("expected '{' after '#jni_env(env)'");
}
const body = try self.parseBlock();
return try self.createNode(start, .{ .jni_env_block = .{
.env = env_expr,
.body = body,
} });
}
fn parseFfiIntrinsicCall(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
const kind: ast.FfiIntrinsicKind = switch (self.current.tag) {

View File

@@ -910,6 +910,12 @@ pub const Analyzer = struct {
.foreign_class_decl => |fd| {
try self.addSymbol(fd.name, .type_alias, null, node.span);
},
.jni_env_block => |eb| {
try self.analyzeNode(eb.env);
try self.pushScope();
try self.analyzeNode(eb.body);
self.popScope();
},
.impl_block => |ib| {
// Each impl block gets its own scope so methods don't conflict across impls
try self.pushScope();
@@ -1311,6 +1317,10 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
.closure_type_expr,
.foreign_class_decl,
=> {},
.jni_env_block => |eb| {
if (findNodeAtOffset(eb.env, offset)) |found| return found;
if (findNodeAtOffset(eb.body, offset)) |found| return found;
},
.struct_decl => |sd| {
for (sd.methods) |method_node| {
if (findNodeAtOffset(method_node, offset)) |found| return found;

View File

@@ -125,6 +125,7 @@ pub const Tag = enum {
hash_extends, // `#extends Alias;` inside a foreign-class body
hash_implements, // `#implements Alias;` inside a foreign-class body
hash_jni_method_descriptor, // `#jni_method_descriptor("(Sig)Ret")` per-method JNI descriptor override
hash_jni_env, // `#jni_env(env) { body }` block-form env-scoping intrinsic
triple_minus, // ---
// Special

View File

@@ -1 +1 @@
1
0

View File

@@ -1 +1 @@
/Users/agra/projects/sx/examples/ffi-jni-env-01-block.sx:17:5: error: unexpected token in expression
inside #jni_env scope