From 32b464e959d52e513d06e84a0abf33ff077846a8 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 20 May 2026 09:24:14 +0300 Subject: [PATCH] ffi 2.1: parser accepts `Foo :: #jni_class("path") { }` opaque form New `hash_jni_class` token + lexer entry, `JniClassDecl` AST node (alias + java path; body deferred to 2.2+), `parseJniClassDecl` consuming `("...") { }` and rejecting non-empty bodies for now. Sema registers the alias as a type_alias symbol; LSP classifies the directive as a keyword. The 2.0 xfail snapshot flips to `parse-only ok`, exit 0. 120/120 examples green; zig test clean. --- src/ast.zig | 8 ++++++ src/lexer.zig | 1 + src/lsp/server.zig | 1 + src/parser.zig | 31 ++++++++++++++++++++++ src/sema.zig | 4 +++ src/token.zig | 1 + tests/expected/ffi-jni-class-01-empty.exit | 2 +- tests/expected/ffi-jni-class-01-empty.txt | 2 +- 8 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/ast.zig b/src/ast.zig index 5f85553..1867aa4 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -81,6 +81,7 @@ pub const Node = struct { protocol_decl: ProtocolDecl, impl_block: ImplBlock, ffi_intrinsic_call: FfiIntrinsicCall, + jni_class_decl: JniClassDecl, pub fn declName(self: Data) ?[]const u8 { return switch (self) { @@ -94,6 +95,7 @@ pub const Node = struct { .ufcs_alias => |d| d.name, .c_import_decl => |d| d.name, .protocol_decl => |d| d.name, + .jni_class_decl => |d| d.name, else => null, }; } @@ -531,6 +533,12 @@ pub const ProtocolDecl = struct { type_params: []const StructTypeParam = &.{}, // for `protocol(Target: Type) { ... }` }; +pub const JniClassDecl = struct { + name: []const u8, // sx-side alias (left of `::`) + java_path: []const u8, // directive arg, e.g. "java/path/Foo" + body: []const *Node = &.{}, // body items (methods, fields, #extends, ...) — empty in 2.1 +}; + pub const ImplBlock = struct { protocol_name: []const u8, target_type: []const u8, diff --git a/src/lexer.zig b/src/lexer.zig index 2f78712..5dc7689 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -82,6 +82,7 @@ pub const Lexer = struct { .{ "#objc_call", Tag.hash_objc_call }, .{ "#jni_call", Tag.hash_jni_call }, .{ "#jni_static_call", Tag.hash_jni_static_call }, + .{ "#jni_class", Tag.hash_jni_class }, }; inline for (directives) |d| { const keyword = d[0]; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index baca0bf..644c64e 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1498,6 +1498,7 @@ pub const Server = struct { .hash_objc_call, .hash_jni_call, .hash_jni_static_call, + .hash_jni_class, => ST.keyword, .kw_f32, .kw_f64, .kw_Type, .kw_Self => ST.type_, diff --git a/src/parser.zig b/src/parser.zig index 4168ab1..19c616e 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -209,6 +209,11 @@ pub const Parser = struct { return self.parseProtocolDecl(name, start_pos); } + // JNI class binding: name :: #jni_class("java/path/Foo") { ...body... } + if (self.current.tag == .hash_jni_class) { + return self.parseJniClassDecl(name, start_pos); + } + // C-style union declaration if (self.current.tag == .kw_union) { return self.parseUnionDecl(name, start_pos); @@ -1022,6 +1027,32 @@ pub const Parser = struct { } }); } + fn parseJniClassDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node { + self.advance(); // skip '#jni_class' + + try self.expect(.l_paren); + if (self.current.tag != .string_literal) { + return self.fail("expected string literal Java class path after '#jni_class('"); + } + const raw = self.tokenSlice(self.current); + const java_path = raw[1 .. raw.len - 1]; + self.advance(); + try self.expect(.r_paren); + + try self.expect(.l_brace); + // Body items (methods, fields, #extends, #implements, ...) land in steps + // 2.2 onward. Step 2.1 accepts only the empty body (opaque forward decl). + if (self.current.tag != .r_brace) { + return self.fail("non-empty `#jni_class` body not yet supported (Phase 2.1 accepts only the empty/opaque form)"); + } + try self.expect(.r_brace); + + return try self.createNode(start_pos, .{ .jni_class_decl = .{ + .name = name, + .java_path = java_path, + } }); + } + fn parseImplBlock(self: *Parser, start_pos: u32) anyerror!*Node { self.advance(); // skip 'impl' diff --git a/src/sema.zig b/src/sema.zig index 8072831..8ae3482 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -907,6 +907,9 @@ pub const Analyzer = struct { } } }, + .jni_class_decl => |jd| { + try self.addSymbol(jd.name, .type_alias, null, node.span); + }, .impl_block => |ib| { // Each impl block gets its own scope so methods don't conflict across impls try self.pushScope(); @@ -1306,6 +1309,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .tuple_type_expr, .ufcs_alias, .closure_type_expr, + .jni_class_decl, => {}, .struct_decl => |sd| { for (sd.methods) |method_node| { diff --git a/src/token.zig b/src/token.zig index 44c5cdf..e1d8845 100644 --- a/src/token.zig +++ b/src/token.zig @@ -115,6 +115,7 @@ pub const Tag = enum { hash_objc_call, // #objc_call(T)(recv, "sel:", args...) hash_jni_call, // #jni_call(T)(env, target, "name", "(Sig)R", args...) hash_jni_static_call, // #jni_static_call(T)(class, "name", "(Sig)R", args...) + hash_jni_class, // Foo :: #jni_class("java/path/Foo") { ...body... } triple_minus, // --- // Special diff --git a/tests/expected/ffi-jni-class-01-empty.exit b/tests/expected/ffi-jni-class-01-empty.exit index d00491f..573541a 100644 --- a/tests/expected/ffi-jni-class-01-empty.exit +++ b/tests/expected/ffi-jni-class-01-empty.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/ffi-jni-class-01-empty.txt b/tests/expected/ffi-jni-class-01-empty.txt index 85040dc..2ef3b99 100644 --- a/tests/expected/ffi-jni-class-01-empty.txt +++ b/tests/expected/ffi-jni-class-01-empty.txt @@ -1 +1 @@ -/Users/agra/projects/sx/examples/ffi-jni-class-01-empty.sx:13:8: error: unexpected token in expression +parse-only ok