diff --git a/examples/ffi-jni-class-01-empty.sx b/examples/ffi-jni-class-01-empty.sx index 2e0f9ea..034ad9a 100644 --- a/examples/ffi-jni-class-01-empty.sx +++ b/examples/ffi-jni-class-01-empty.sx @@ -1,5 +1,5 @@ // Phase 2 step 2.0 (PLAN-FFI.md): xfail parser test for the -// type-introducer `Foo :: #jni_class("java/path/Foo") { }` directive. +// type-introducer `Foo :: #foreign #jni_class("java/path/Foo") { }` directive. // // Today's parser doesn't recognize `#jni_class` as a known hash // directive after `::`, so it falls through to expression parsing @@ -10,7 +10,7 @@ #import "modules/std.sx"; -Foo :: #jni_class("java/path/Foo") { } +Foo :: #foreign #jni_class("java/path/Foo") { } main :: () -> s32 { print("parse-only ok\n"); diff --git a/examples/ffi-jni-class-02-method.sx b/examples/ffi-jni-class-02-method.sx index ed3d2ea..dc6d6ee 100644 --- a/examples/ffi-jni-class-02-method.sx +++ b/examples/ffi-jni-class-02-method.sx @@ -10,7 +10,7 @@ // `Self` here refers to the enclosing `View` type — resolved at // 2.x sema, not at parse time. -View :: #jni_class("android/view/View") { +View :: #foreign #jni_class("android/view/View") { getId :: (self: *Self) -> s32; } diff --git a/examples/ffi-jni-class-03-static.sx b/examples/ffi-jni-class-03-static.sx index 040b660..d8f62d2 100644 --- a/examples/ffi-jni-class-03-static.sx +++ b/examples/ffi-jni-class-03-static.sx @@ -9,7 +9,7 @@ #import "modules/std.sx"; -Math :: #jni_class("java/lang/Math") { +Math :: #foreign #jni_class("java/lang/Math") { static abs :: (n: s32) -> s32; } diff --git a/examples/ffi-jni-class-04-extends.sx b/examples/ffi-jni-class-04-extends.sx index d112f68..bfe4709 100644 --- a/examples/ffi-jni-class-04-extends.sx +++ b/examples/ffi-jni-class-04-extends.sx @@ -12,9 +12,9 @@ #import "modules/std.sx"; -View :: #jni_class("android/view/View") { } +View :: #foreign #jni_class("android/view/View") { } -Window :: #jni_class("android/view/Window") { +Window :: #foreign #jni_class("android/view/Window") { #extends View; getDecorView :: (self: *Self) -> *View; } diff --git a/examples/ffi-jni-class-05-field.sx b/examples/ffi-jni-class-05-field.sx index 46220f8..30c2607 100644 --- a/examples/ffi-jni-class-05-field.sx +++ b/examples/ffi-jni-class-05-field.sx @@ -8,7 +8,7 @@ #import "modules/std.sx"; -Point :: #jni_class("android/graphics/Point") { +Point :: #foreign #jni_class("android/graphics/Point") { x: s32; y: s32; } diff --git a/examples/ffi-jni-class-06-desc.sx b/examples/ffi-jni-class-06-desc.sx index d989a47..e378edf 100644 --- a/examples/ffi-jni-class-06-desc.sx +++ b/examples/ffi-jni-class-06-desc.sx @@ -10,7 +10,7 @@ #import "modules/std.sx"; -Foo :: #jni_class("com/example/Foo") { +Foo :: #foreign #jni_class("com/example/Foo") { weirdMethod :: (self: *Self) -> s32 #jni_method_descriptor("()I"); } diff --git a/examples/ffi-jni-class-07-all-runtimes.sx b/examples/ffi-jni-class-07-all-runtimes.sx index d0ec4dc..72ef9b4 100644 --- a/examples/ffi-jni-class-07-all-runtimes.sx +++ b/examples/ffi-jni-class-07-all-runtimes.sx @@ -17,27 +17,27 @@ #import "modules/std.sx"; -IFoo :: #jni_interface("com/example/IFoo") { +IFoo :: #foreign #jni_interface("com/example/IFoo") { bar :: (self: *Self) -> s32; } -NSString :: #objc_class("NSString") { +NSString :: #foreign #objc_class("NSString") { length :: (self: *Self) -> s32; } -NSCopying :: #objc_protocol("NSCopying") { +NSCopying :: #foreign #objc_protocol("NSCopying") { copy :: (self: *Self) -> *Self; } -URL :: #swift_class("Foundation.URL") { +URL :: #foreign #swift_class("Foundation.URL") { absoluteString :: (self: *Self) -> *void; } -Date :: #swift_struct("Foundation.Date") { +Date :: #foreign #swift_struct("Foundation.Date") { timeIntervalSince1970 :: (self: *Self) -> f64; } -Hashable :: #swift_protocol("Swift.Hashable") { +Hashable :: #foreign #swift_protocol("Swift.Hashable") { hash :: (self: *Self) -> s32; } diff --git a/examples/ffi-jni-class-08-call.sx b/examples/ffi-jni-class-08-call.sx index dd1fefa..03dc1e4 100644 --- a/examples/ffi-jni-class-08-call.sx +++ b/examples/ffi-jni-class-08-call.sx @@ -8,7 +8,7 @@ #import "modules/std.sx"; -Activity :: #jni_class("android/app/Activity") { +Activity :: #foreign #jni_class("android/app/Activity") { getWindow :: (self: *Self) -> *void; } diff --git a/library/modules/platform/android_jni.sx b/library/modules/platform/android_jni.sx index fdb6b1a..728a5cf 100644 --- a/library/modules/platform/android_jni.sx +++ b/library/modules/platform/android_jni.sx @@ -11,21 +11,21 @@ // signatures (`getWindow :: (self: *Self) -> *Window`) still resolve // against the bare name within the namespace. -WindowInsets :: #jni_class("android/view/WindowInsets") { +WindowInsets :: #foreign #jni_class("android/view/WindowInsets") { getSystemWindowInsetTop :: (self: *Self) -> s32; getSystemWindowInsetLeft :: (self: *Self) -> s32; getSystemWindowInsetBottom :: (self: *Self) -> s32; getSystemWindowInsetRight :: (self: *Self) -> s32; } -View :: #jni_class("android/view/View") { +View :: #foreign #jni_class("android/view/View") { getRootWindowInsets :: (self: *Self) -> *WindowInsets; } -Window :: #jni_class("android/view/Window") { +Window :: #foreign #jni_class("android/view/Window") { getDecorView :: (self: *Self) -> *View; } -Activity :: #jni_class("android/app/Activity") { +Activity :: #foreign #jni_class("android/app/Activity") { getWindow :: (self: *Self) -> *Window; } diff --git a/src/ast.zig b/src/ast.zig index 09a34f4..72c98d6 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -551,6 +551,7 @@ pub const ForeignMethodDecl = struct { return_type: ?*Node, // null = void is_static: bool = false, // true for `static name :: ...` jni_descriptor_override: ?[]const u8 = null, // `#jni_method_descriptor("(Sig)Ret")` — JNI runtime only + body: ?*Node = null, // sx-side implementation (defined-class only). null = `;`-terminated decl referencing inherited / external method. }; pub const ForeignFieldDecl = struct { @@ -570,6 +571,8 @@ pub const ForeignClassDecl = struct { foreign_path: []const u8, // directive arg: "java/path/Foo" / "NSString" / "Foundation.URL" runtime: ForeignRuntime, members: []const ForeignClassMember = &.{}, + is_foreign: bool = false, // `#foreign #...` prefix — class is provided by the foreign runtime; we only reference it + is_main: bool = false, // `#jni_main` / `#objc_main` — class is the launchable entry (Activity / UIApplicationDelegate / ...) }; pub const JniEnvBlock = struct { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index dd38ffe..f6da606 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -3972,6 +3972,13 @@ pub const Lowering = struct { return Ref.none; }; + if (!fcd.is_foreign) { + if (self.diagnostics) |d| { + d.addFmt(.err, span, "sx-defined foreign classes can't yet be dispatched into (class '{s}' missing '#foreign' modifier? — runtime synthesis is a follow-up)", .{fcd.name}); + } + return Ref.none; + } + if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { if (self.diagnostics) |d| { d.addFmt(.err, span, "method calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); diff --git a/src/lexer.zig b/src/lexer.zig index ccaf21e..ab3e93d 100644 --- a/src/lexer.zig +++ b/src/lexer.zig @@ -93,6 +93,7 @@ pub const Lexer = struct { .{ "#implements", Tag.hash_implements }, .{ "#jni_method_descriptor", Tag.hash_jni_method_descriptor }, .{ "#jni_env", Tag.hash_jni_env }, + .{ "#jni_main", Tag.hash_jni_main }, }; inline for (directives) |d| { const keyword = d[0]; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index 9f835e3..adae3fc 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1509,6 +1509,7 @@ pub const Server = struct { .hash_implements, .hash_jni_method_descriptor, .hash_jni_env, + .hash_jni_main, => ST.keyword, .kw_f32, .kw_f64, .kw_Type, .kw_Self => ST.type_, diff --git a/src/parser.zig b/src/parser.zig index af849be..f563510 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -209,10 +209,15 @@ pub const Parser = struct { return self.parseProtocolDecl(name, start_pos); } - // Foreign-type binding: name :: #jni_class / #jni_interface / #objc_class / - // #objc_protocol / #swift_class / #swift_struct / #swift_protocol ("path") { body } - if (self.foreignRuntimeForCurrent()) |runtime| { - return self.parseForeignClassDecl(name, start_pos, runtime); + // Foreign-type binding with optional prefix modifiers: + // [#foreign | #jni_main]* (#jni_class / #jni_interface / #objc_class / + // #objc_protocol / #swift_class / #swift_struct / #swift_protocol) ("path") { body } + // + // Define-by-default: bare `#jni_class("...")` declares a new class (sx-defined). + // `#foreign` flips that to "reference an existing class on the foreign side." + // `#jni_main` flags the class as the launchable entry (Android Activity). + if (self.tryParseForeignClassPrefix()) |prefix| { + return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main); } // C-style union declaration @@ -1041,7 +1046,70 @@ pub const Parser = struct { }; } - fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime) anyerror!*Node { + const ForeignClassPrefix = struct { + runtime: ast.ForeignRuntime, + is_foreign: bool, + is_main: bool, + }; + + /// Recognise an optional sequence of `#foreign` / `#jni_main` modifiers + /// followed by a type-introducer directive (`#jni_class`, `#objc_class`, + /// ...). Returns null if the current position isn't a foreign-class + /// directive (possibly after modifiers). Consumes the modifier tokens + /// only when a runtime directive follows; otherwise leaves the parser + /// state untouched. + fn tryParseForeignClassPrefix(self: *Parser) ?ForeignClassPrefix { + // Peek ahead through modifier tokens to confirm a directive follows. + var lookahead_idx: usize = 0; + var is_foreign = false; + var is_main = false; + while (true) { + const tag = self.peekTag(lookahead_idx); + switch (tag) { + .hash_foreign => { + is_foreign = true; + lookahead_idx += 1; + }, + .hash_jni_main => { + is_main = true; + lookahead_idx += 1; + }, + else => break, + } + } + const runtime = self.foreignRuntimeForOffset(lookahead_idx) orelse return null; + // Commit: consume modifier tokens. + var i: usize = 0; + while (i < lookahead_idx) : (i += 1) self.advance(); + return .{ .runtime = runtime, .is_foreign = is_foreign, .is_main = is_main }; + } + + fn peekTag(self: *Parser, offset: usize) Tag { + if (offset == 0) return self.current.tag; + var lexer_copy = self.lexer; + var tok: Token = undefined; + var i: usize = 0; + while (i < offset) : (i += 1) { + tok = lexer_copy.next(); + } + return tok.tag; + } + + fn foreignRuntimeForOffset(self: *Parser, offset: usize) ?ast.ForeignRuntime { + const tag = self.peekTag(offset); + return switch (tag) { + .hash_jni_class => .jni_class, + .hash_jni_interface => .jni_interface, + .hash_objc_class => .objc_class, + .hash_objc_protocol => .objc_protocol, + .hash_swift_class => .swift_class, + .hash_swift_struct => .swift_struct, + .hash_swift_protocol => .swift_protocol, + else => null, + }; + } + + fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool) anyerror!*Node { self.advance(); // skip directive token try self.expect(.l_paren); @@ -1149,7 +1217,15 @@ pub const Parser = struct { try self.expect(.r_paren); } - try self.expect(.semicolon); + // Method body is optional: `;` → declaration (foreign or inherited + // method we just want to call); `{ ... }` → sx-side implementation + // for sx-defined classes. + var body_node: ?*Node = null; + if (self.current.tag == .l_brace) { + body_node = try self.parseBlock(); + } else { + try self.expect(.semicolon); + } try members.append(self.allocator, .{ .method = .{ .name = member_name, @@ -1158,6 +1234,7 @@ pub const Parser = struct { .return_type = return_type, .is_static = is_static, .jni_descriptor_override = desc_override, + .body = body_node, } }); } try self.expect(.r_brace); @@ -1167,6 +1244,8 @@ pub const Parser = struct { .foreign_path = foreign_path, .runtime = runtime, .members = try members.toOwnedSlice(self.allocator), + .is_foreign = is_foreign, + .is_main = is_main, } }); } diff --git a/src/sema.zig b/src/sema.zig index ee95223..3e20c72 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -909,6 +909,25 @@ pub const Analyzer = struct { }, .foreign_class_decl => |fd| { try self.addSymbol(fd.name, .type_alias, null, node.span); + if (fd.is_foreign and fd.is_main) { + try self.diagnostics.append(self.allocator, .{ + .level = .err, + .message = "'#foreign' and '#jni_main' / '#objc_main' are mutually exclusive — a foreign-referenced class can't be the app's main entry", + .span = node.span, + }); + } + if (fd.is_foreign) { + for (fd.members) |m| switch (m) { + .method => |md| if (md.body != null) { + try self.diagnostics.append(self.allocator, .{ + .level = .err, + .message = "methods on a '#foreign' class can't have bodies — they reference foreign-runtime implementations", + .span = node.span, + }); + }, + else => {}, + }; + } }, .jni_env_block => |eb| { try self.analyzeNode(eb.env); diff --git a/src/token.zig b/src/token.zig index 5e312e5..3d5e14b 100644 --- a/src/token.zig +++ b/src/token.zig @@ -126,6 +126,7 @@ pub const Tag = enum { 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 + hash_jni_main, // `#jni_main #jni_class(...) { ... }` — class is the launchable Android Activity triple_minus, // --- // Special