ffi #jni_main slice 1: Java source emitter (pure fn + unit tests)
`src/ir/jni_java_emit.zig`'s `emitJavaSource` takes a
`ForeignClassDecl` with `is_main = true` and returns the `.java`
source text. AOT pipeline integration (javac + d8 + APK bundling +
manifest synthesis + RegisterNatives) lands in subsequent slices.
Emission shape per bodied method:
@Override
public <ret> <name>(<params>) {
super.<name>(<args>);
sx_<name>(<args>);
}
private native <ret> sx_<name>(<params>);
Declaration-only methods (no body — references inherited Java
methods that sx wants to *call*) are skipped — no override, no
native delegate.
`#extends Alias` resolves through the supplied class registry to
the parent's foreign Java path. Default parent is
`android.app.NativeActivity` when `#extends` is absent.
Type matrix: primitives (void/bool/s8..s64/u8/u16/f32/f64), `*Self`
elided as the receiver (Java's implicit `this`), `*void` as
`Object`, `*Foo` cross-class refs resolved through the class
registry.
Six unit tests cover: non-main rejection, full void onCreate(Bundle)
shape with @Override delegate, primitive params, declaration-only
skipping, `#extends Alias` resolution, default-package classes.
130/130 examples still green; zig test clean.
This commit is contained in:
237
src/ir/jni_java_emit.test.zig
Normal file
237
src/ir/jni_java_emit.test.zig
Normal file
@@ -0,0 +1,237 @@
|
||||
// Tests for jni_java_emit.zig — #jni_main pipeline slice 1.
|
||||
// Locks in the Java source emitted from `ForeignClassDecl` AST nodes:
|
||||
// package split, class header, @Override delegate pattern, primitive
|
||||
// type mapping, cross-class refs through the foreign_class registry.
|
||||
|
||||
const std = @import("std");
|
||||
const ast = @import("../ast.zig");
|
||||
const emit = @import("jni_java_emit.zig");
|
||||
|
||||
const Node = ast.Node;
|
||||
|
||||
fn makeTypeExpr(allocator: std.mem.Allocator, name: []const u8) !*Node {
|
||||
const node = try allocator.create(Node);
|
||||
node.* = .{
|
||||
.span = .{ .start = 0, .end = 0 },
|
||||
.data = .{ .type_expr = .{ .name = name } },
|
||||
};
|
||||
return node;
|
||||
}
|
||||
|
||||
fn makePointer(allocator: std.mem.Allocator, pointee: *Node) !*Node {
|
||||
const node = try allocator.create(Node);
|
||||
node.* = .{
|
||||
.span = .{ .start = 0, .end = 0 },
|
||||
.data = .{ .pointer_type_expr = .{ .pointee_type = pointee } },
|
||||
};
|
||||
return node;
|
||||
}
|
||||
|
||||
/// Marker for "method has a body" — emitJavaSource only checks
|
||||
/// `body != null`. The actual node contents are unused.
|
||||
fn makeBodyMarker(allocator: std.mem.Allocator) !*Node {
|
||||
const node = try allocator.create(Node);
|
||||
node.* = .{
|
||||
.span = .{ .start = 0, .end = 0 },
|
||||
.data = .{ .block = .{ .stmts = &.{} } },
|
||||
};
|
||||
return node;
|
||||
}
|
||||
|
||||
test "rejects non-main decl" {
|
||||
const a = std.testing.allocator;
|
||||
const fcd: ast.ForeignClassDecl = .{
|
||||
.name = "Foo",
|
||||
.foreign_path = "co/example/Foo",
|
||||
.runtime = .jni_class,
|
||||
.is_main = false, // ← not main
|
||||
};
|
||||
const result = emit.emitJavaSource(a, &fcd, .{});
|
||||
try std.testing.expectError(emit.EmitError.NotAJniMainClass, result);
|
||||
}
|
||||
|
||||
test "void onCreate(Bundle) with #extends NativeActivity default" {
|
||||
const a = std.testing.allocator;
|
||||
var arena = std.heap.ArenaAllocator.init(a);
|
||||
defer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
|
||||
const bundle_ty = try makePointer(aa, try makeTypeExpr(aa, "Bundle"));
|
||||
const body = try makeBodyMarker(aa);
|
||||
|
||||
var registry = std.StringHashMap([]const u8).init(a);
|
||||
defer registry.deinit();
|
||||
try registry.put("Bundle", "android/os/Bundle");
|
||||
|
||||
const member: ast.ForeignClassMember = .{ .method = .{
|
||||
.name = "onCreate",
|
||||
.params = &.{ self_ty, bundle_ty },
|
||||
.param_names = &.{ "self", "b" },
|
||||
.return_type = null,
|
||||
.body = body,
|
||||
} };
|
||||
const fcd: ast.ForeignClassDecl = .{
|
||||
.name = "SxApp",
|
||||
.foreign_path = "co/swipelab/sx_runtime/SxNativeActivity",
|
||||
.runtime = .jni_class,
|
||||
.is_main = true,
|
||||
.members = &.{member},
|
||||
};
|
||||
|
||||
const out = try emit.emitJavaSource(a, &fcd, .{ .classes = ®istry });
|
||||
defer a.free(out);
|
||||
|
||||
const expected =
|
||||
\\package co.swipelab.sx_runtime;
|
||||
\\
|
||||
\\public class SxNativeActivity extends android.app.NativeActivity {
|
||||
\\ @Override
|
||||
\\ public void onCreate(android.os.Bundle b) {
|
||||
\\ super.onCreate(b);
|
||||
\\ sx_onCreate(b);
|
||||
\\ }
|
||||
\\ private native void sx_onCreate(android.os.Bundle b);
|
||||
\\}
|
||||
\\
|
||||
;
|
||||
try std.testing.expectEqualStrings(expected, out);
|
||||
}
|
||||
|
||||
test "primitive params" {
|
||||
const a = std.testing.allocator;
|
||||
var arena = std.heap.ArenaAllocator.init(a);
|
||||
defer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
|
||||
const bool_ty = try makeTypeExpr(aa, "bool");
|
||||
const body = try makeBodyMarker(aa);
|
||||
|
||||
const member: ast.ForeignClassMember = .{ .method = .{
|
||||
.name = "onWindowFocusChanged",
|
||||
.params = &.{ self_ty, bool_ty },
|
||||
.param_names = &.{ "self", "hasFocus" },
|
||||
.return_type = null,
|
||||
.body = body,
|
||||
} };
|
||||
const fcd: ast.ForeignClassDecl = .{
|
||||
.name = "Sx",
|
||||
.foreign_path = "co/sample/Sx",
|
||||
.runtime = .jni_class,
|
||||
.is_main = true,
|
||||
.members = &.{member},
|
||||
};
|
||||
const out = try emit.emitJavaSource(a, &fcd, .{});
|
||||
defer a.free(out);
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "public void onWindowFocusChanged(boolean hasFocus)") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "super.onWindowFocusChanged(hasFocus);") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "sx_onWindowFocusChanged(hasFocus);") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "private native void sx_onWindowFocusChanged(boolean hasFocus);") != null);
|
||||
}
|
||||
|
||||
test "declaration-only methods are skipped" {
|
||||
const a = std.testing.allocator;
|
||||
var arena = std.heap.ArenaAllocator.init(a);
|
||||
defer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
|
||||
const body = try makeBodyMarker(aa);
|
||||
|
||||
// One bodied (override), one declaration-only (calls inherited).
|
||||
const bodied: ast.ForeignClassMember = .{ .method = .{
|
||||
.name = "onCreate",
|
||||
.params = &.{self_ty},
|
||||
.param_names = &.{"self"},
|
||||
.return_type = null,
|
||||
.body = body,
|
||||
} };
|
||||
const decl_only: ast.ForeignClassMember = .{ .method = .{
|
||||
.name = "finish",
|
||||
.params = &.{self_ty},
|
||||
.param_names = &.{"self"},
|
||||
.return_type = null,
|
||||
.body = null, // sx-side just *calls* this; Java's NativeActivity.finish() provides it
|
||||
} };
|
||||
|
||||
const fcd: ast.ForeignClassDecl = .{
|
||||
.name = "Sx",
|
||||
.foreign_path = "co/example/Sx",
|
||||
.runtime = .jni_class,
|
||||
.is_main = true,
|
||||
.members = &.{ bodied, decl_only },
|
||||
};
|
||||
const out = try emit.emitJavaSource(a, &fcd, .{});
|
||||
defer a.free(out);
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "sx_onCreate") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "sx_finish") == null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "void finish(") == null);
|
||||
}
|
||||
|
||||
test "#extends Alias resolves through class registry" {
|
||||
const a = std.testing.allocator;
|
||||
var arena = std.heap.ArenaAllocator.init(a);
|
||||
defer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
|
||||
const body = try makeBodyMarker(aa);
|
||||
|
||||
const extends_member: ast.ForeignClassMember = .{ .extends = "MyParent" };
|
||||
const method_member: ast.ForeignClassMember = .{ .method = .{
|
||||
.name = "onCreate",
|
||||
.params = &.{self_ty},
|
||||
.param_names = &.{"self"},
|
||||
.return_type = null,
|
||||
.body = body,
|
||||
} };
|
||||
|
||||
var registry = std.StringHashMap([]const u8).init(a);
|
||||
defer registry.deinit();
|
||||
try registry.put("MyParent", "co/example/MyParentActivity");
|
||||
|
||||
const fcd: ast.ForeignClassDecl = .{
|
||||
.name = "Sx",
|
||||
.foreign_path = "co/example/Sx",
|
||||
.runtime = .jni_class,
|
||||
.is_main = true,
|
||||
.members = &.{ extends_member, method_member },
|
||||
};
|
||||
const out = try emit.emitJavaSource(a, &fcd, .{ .classes = ®istry });
|
||||
defer a.free(out);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "extends co.example.MyParentActivity") != null);
|
||||
}
|
||||
|
||||
test "default-package class (no slash in foreign_path)" {
|
||||
const a = std.testing.allocator;
|
||||
var arena = std.heap.ArenaAllocator.init(a);
|
||||
defer arena.deinit();
|
||||
const aa = arena.allocator();
|
||||
|
||||
const self_ty = try makePointer(aa, try makeTypeExpr(aa, "Self"));
|
||||
const body = try makeBodyMarker(aa);
|
||||
|
||||
const member: ast.ForeignClassMember = .{ .method = .{
|
||||
.name = "onCreate",
|
||||
.params = &.{self_ty},
|
||||
.param_names = &.{"self"},
|
||||
.return_type = null,
|
||||
.body = body,
|
||||
} };
|
||||
const fcd: ast.ForeignClassDecl = .{
|
||||
.name = "Sx",
|
||||
.foreign_path = "SxNoPackage",
|
||||
.runtime = .jni_class,
|
||||
.is_main = true,
|
||||
.members = &.{member},
|
||||
};
|
||||
const out = try emit.emitJavaSource(a, &fcd, .{});
|
||||
defer a.free(out);
|
||||
|
||||
// No `package ...;` line when the foreign path has no slashes.
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "package ") == null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, out, "public class SxNoPackage") != null);
|
||||
}
|
||||
Reference in New Issue
Block a user