ffi: define-by-default #jni_class + #foreign modifier + #jni_main token
Flip the surface semantics for type-introducer directives: bare
`Foo :: #jni_class("path") { ... }` now means "DEFINE a new Java class
at that path" (sx-side provides the implementations). The `#foreign`
prefix modifier flips it back to "REFERENCE an existing class on the
foreign runtime." Matches how `#foreign` already reads in sx for C
function declarations (`printf :: ... #foreign;`).
Foo :: #foreign #jni_class("path/to/Foo") { ... } // reference
Foo :: #jni_class("path/to/Foo") { ... } // define
Foo :: #jni_main #jni_class("path/to/Foo") { ... } // define + main Activity
Compiler-side changes:
- New `hash_jni_main` lexer token (the launchable-Activity marker).
Existing `hash_foreign` is reused; no new modifier token there.
- `ForeignClassDecl` gains `is_foreign: bool` + `is_main: bool`.
`ForeignMethodDecl` gains `body: ?*Node` so defined-class methods
can carry sx-side implementations (foreign-class methods stay
`;`-terminated).
- Parser learns `tryParseForeignClassPrefix` — peek-and-consume the
modifier tokens, then dispatch to the unchanged
`parseForeignClassDecl` with the flags threaded through.
- Sema rejects two illegal combinations: `#foreign + #jni_main`
(can't be both an external reference and the app's main entry),
and bodied methods on `#foreign` decls (foreign methods are
runtime-provided).
- Lower's foreign-class dispatch errors on non-foreign decls with
a pointer to the runtime-synthesis follow-up; defined-class
codegen (Java class emission, RegisterNatives wiring, manifest
entry generation) lands in a separate session.
Migration:
- `library/modules/platform/android_jni.sx`: all four foreign class
decls (`Activity`, `Window`, `View`, `WindowInsets`) gain `#foreign`.
- `examples/ffi-jni-class-{01..08}*.sx`: every test's `#jni_class` /
`#jni_interface` / `#objc_class` / `#objc_protocol` / `#swift_class`
/ `#swift_struct` / `#swift_protocol` usage gains `#foreign`. All
9 files mechanical perl rename; snapshots unchanged.
Verified locally:
- `zig build test` clean.
- `bash tests/run_examples.sh` 129/129.
- `bash tests/cross_compile.sh` 3/3.
- Chess APK rebuilds, reinstalls, launches on Pixel; safe-area
clearance preserved.
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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_,
|
||||
|
||||
@@ -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,
|
||||
} });
|
||||
}
|
||||
|
||||
|
||||
19
src/sema.zig
19
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user