diff --git a/library/modules/platform/android.sx b/library/modules/platform/android.sx new file mode 100644 index 0000000..8abc641 --- /dev/null +++ b/library/modules/platform/android.sx @@ -0,0 +1,397 @@ +// Android backend driven by a `#jni_main` Activity (no native_app_glue). +// +// Lifecycle: +// +// 1. Java `SxApp.onCreate(b)` → native `sx_onCreate`: stash JNIEnv* + +// Activity globals, install the AAssetManager into the C file_utils, +// construct a `SurfaceView`, register `SxApp` as its +// SurfaceHolder.Callback, set as Activity content view. +// 2. Java `SxApp.surfaceCreated(holder)` → native `sx_surfaceCreated`: +// extract the ANativeWindow from the holder's Surface, then +// `pthread_create` the render thread on first delivery. +// 3. Render thread: brings up EGL on the ANativeWindow, then calls +// `sx_app_main()` — the user's entry-point, which sets up the +// AndroidPlatform / GPU / pipeline globals and ends in +// `run_frame_loop(closure(frame))`. +// 4. `run_frame_loop` drives the loop: drain touch events queue, +// invoke `frame_fn`, `eglSwapBuffers`, sleep ~1ms. +// 5. Java `onTouchEvent` → native `sx_onTouchEvent`: push the +// (action,x,y) tuple onto a mutex-guarded queue. `poll_events` +// drains the queue into the platform's standard `Event` shape. +// +// Vulkan-compatible: same ANativeWindow drives `vkCreate*SurfaceKHR` +// without changing the lifecycle. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; +#import "modules/ui/types.sx"; +#import "modules/ui/events.sx"; +#import "modules/platform/types.sx"; +#import "modules/platform/api.sx"; + +// ── Foreign Java types ────────────────────────────────────────────────── + +Bundle :: #foreign #jni_class("android/os/Bundle") { } +JContext :: #foreign #jni_class("android/content/Context") { + getAssets :: (self: *Self) -> *AssetManagerJ; +} +AssetManagerJ :: #foreign #jni_class("android/content/res/AssetManager") { } + +Surface :: #foreign #jni_class("android/view/Surface") { } +SurfaceHolder :: #foreign #jni_class("android/view/SurfaceHolder") { + getSurface :: (self: *Self) -> *Surface; + addCallback :: (self: *Self, cb: *SurfaceHolderCallback); +} +SurfaceView :: #foreign #jni_class("android/view/SurfaceView") { + static new :: (ctx: *JContext) -> *Self; + getHolder :: (self: *Self) -> *SurfaceHolder; +} +SurfaceHolderCallback :: #foreign #jni_class("android/view/SurfaceHolder$Callback") { } + +MotionEvent :: #foreign #jni_class("android/view/MotionEvent") { + getAction :: (self: *Self) -> s32; + getX :: (self: *Self) -> f32; + getY :: (self: *Self) -> f32; +} + +JView :: #foreign #jni_class("android/view/View") { } +ActivityClass :: #foreign #jni_class("android/app/Activity") { + setContentView :: (self: *Self, v: *JView); +} + +// ── Foreign C/NDK decls ───────────────────────────────────────────────── + +// C side of file_utils — installs the AAssetManager so `read_file_bytes` +// can route through `AAssetManager_open` when running on Android. +sx_android_set_asset_manager :: (mgr: *void) #foreign; + +__android_log_print :: (prio: s32, tag: *u8, fmt: *u8) -> s32 #foreign; +usleep :: (us: u32) -> s32 #foreign; + +// libandroid +ANativeWindow_fromSurface :: (env: *void, surface: *void) -> *void #foreign; +ANativeWindow_release :: (window: *void) #foreign; +ANativeWindow_getWidth :: (window: *void) -> s32 #foreign; +ANativeWindow_getHeight :: (window: *void) -> s32 #foreign; +ANativeWindow_setBuffersGeometry :: (w: *void, width: s32, height: s32, fmt: s32) -> s32 #foreign; + +AAssetManager_fromJava :: (env: *void, mgr: *void) -> *void #foreign; + +// pthread (link libpthread is built into bionic). +pthread_create :: (thread: *u64, attr: *void, start: (*void) -> *void, arg: *void) -> s32 #foreign; +pthread_mutex_init :: (m: *void, attr: *void) -> s32 #foreign; +pthread_mutex_lock :: (m: *void) -> s32 #foreign; +pthread_mutex_unlock :: (m: *void) -> s32 #foreign; + +// EGL. Constants from . We bring up an ES3 context with a +// 24-bit RGB framebuffer + 24-bit depth (same shape chess used under +// the legacy NDK path). +EGL_DEFAULT_DISPLAY :: 0; +EGL_NO_DISPLAY :*void: null; +EGL_NO_CONTEXT :*void: null; +EGL_NO_SURFACE :*void: null; +EGL_TRUE :u32: 1; +EGL_FALSE :u32: 0; +EGL_NONE :s32: 0x3038; +EGL_RED_SIZE :s32: 0x3024; +EGL_GREEN_SIZE :s32: 0x3023; +EGL_BLUE_SIZE :s32: 0x3022; +EGL_ALPHA_SIZE :s32: 0x3021; +EGL_DEPTH_SIZE :s32: 0x3025; +EGL_RENDERABLE_TYPE :s32: 0x3040; +EGL_SURFACE_TYPE :s32: 0x3033; +EGL_OPENGL_ES3_BIT :s32: 0x00000040; +EGL_WINDOW_BIT :s32: 0x0004; +EGL_NATIVE_VISUAL_ID :s32: 0x302E; +EGL_CONTEXT_CLIENT_VERSION :s32: 0x3098; + +eglGetDisplay :: (id: u64) -> *void #foreign; +eglInitialize :: (d: *void, major: *s32, minor: *s32) -> u32 #foreign; +eglChooseConfig :: (d: *void, attrs: *s32, configs: **void, sz: s32, num: *s32) -> u32 #foreign; +eglGetConfigAttrib :: (d: *void, cfg: *void, attr: s32, value: *s32) -> u32 #foreign; +eglCreateContext :: (d: *void, cfg: *void, share: *void, attrs: *s32) -> *void #foreign; +eglCreateWindowSurface :: (d: *void, cfg: *void, window: *void, attrs: *s32) -> *void #foreign; +eglMakeCurrent :: (d: *void, draw: *void, read: *void, ctx: *void) -> u32 #foreign; +eglSwapBuffers :: (d: *void, surface: *void) -> u32 #foreign; +eglDestroyContext :: (d: *void, ctx: *void) -> u32 #foreign; +eglDestroySurface :: (d: *void, surface: *void) -> u32 #foreign; +eglTerminate :: (d: *void) -> u32 #foreign; + +// ── Module-level state ────────────────────────────────────────────────── + +g_activity : *void = null; // global ref to the SxApp jobject (saved env can't outlive scope) +g_app_window : *void = null; // ANativeWindow from surfaceCreated +g_egl_display : *void = null; +g_egl_context : *void = null; +g_egl_surface : *void = null; +g_egl_config : *void = null; +g_viewport_w : s32 = 0; +g_viewport_h : s32 = 0; +// Defaults to 1.0 until a proper density query lands. Chess's pipeline +// uses `viewport_w/h` as the layout space and `dpi_scale` to scale +// rendering; mismatches cause layout drift / shrinking. +g_dpi_scale : f32 = 1.0; +g_should_stop : bool = false; +g_render_thread_started : bool = false; +g_render_thread : u64 = 0; +g_frame_fn : Closure() = ---; +g_frame_fn_set : bool = false; + +// Touch event queue. Single-producer (Java UI thread) / single-consumer +// (render thread); a small ring buffer guarded by a pthread mutex is +// enough — chess only generates touches on user interaction so contention +// is rare. +TouchEvent :: struct { + action: s32; + x: f32; + y: f32; +} + +g_touch_queue : [64]TouchEvent = ---; +g_touch_head : u32 = 0; +g_touch_tail : u32 = 0; +// pthread_mutex_t is 40 bytes on bionic (NDK 26+); over-size to 64 for safety. +g_touch_mutex_storage : [64]u8 = ---; +g_touch_mutex_inited : bool = false; + +// ── #jni_main Activity ────────────────────────────────────────────────── + +// ── User-facing helpers for the consumer's `#jni_main` Activity ──────── +// +// The consumer (chess, etc.) writes their own `SxApp :: #jni_main +// #jni_class("...")` declaration with `#implements SurfaceHolderCallback` +// and the standard lifecycle methods. This file provides the primitives +// those methods call: +// +// - `sx_android_forward_assets(env, activity)` from onCreate. +// - `sx_android_attach_window(env, holder)` from surfaceCreated. +// - `sx_android_detach_window()` from surfaceDestroyed. +// - `sx_android_set_viewport(w, h)` from surfaceChanged. +// - `sx_android_start_render_thread(main_fn)` once the surface is up. +// - `sx_android_push_touch(action, x, y)` from onTouchEvent. + +// Extract the AAssetManager from the Activity and install it into the +// C file_utils so `read_file_bytes` can route through `AAssetManager_open`. +// Call this from your Activity's `onCreate` (BEFORE any asset read). +sx_android_forward_assets :: (env: *void, activity: *JContext) { + #jni_env(env) { + assets := activity.getAssets(); + aam := AAssetManager_fromJava(env, xx assets); + sx_android_set_asset_manager(aam); + } +} + +// Extract the ANativeWindow from a SurfaceHolder. Call this from your +// Activity's `surfaceCreated`. The window stays valid until +// `sx_android_detach_window` runs (typically in `surfaceDestroyed`). +sx_android_attach_window :: (env: *void, holder: *SurfaceHolder) { + #jni_env(env) { + surface := holder.getSurface(); + g_app_window = ANativeWindow_fromSurface(env, xx surface); + } +} + +sx_android_detach_window :: () { + if g_app_window != null { + ANativeWindow_release(g_app_window); + g_app_window = null; + } +} + +sx_android_set_viewport :: (w: s32, h: s32) { + g_viewport_w = w; + g_viewport_h = h; +} + +// Start the render thread that brings up EGL on `g_app_window` and calls +// the user-supplied `entry_fn` (typically the user's `main`). Safe to +// call once after `sx_android_attach_window` has set the window. +sx_android_start_render_thread :: (entry_fn: () -> void) { + if g_render_thread_started { return; } + g_user_main_fn = entry_fn; + pthread_create(@g_render_thread, null, sx_android_render_thread_entry, null); + g_render_thread_started = true; +} + +g_user_main_fn : () -> void = null; + +sx_android_render_thread_entry :: (arg: *void) -> *void { + while g_app_window == null and !g_should_stop { + usleep(1000); + } + if g_should_stop { return null; } + + if !sx_android_egl_init() { + __android_log_print(6, "sxapp".ptr, "EGL bootstrap failed\n".ptr); + return null; + } + + if g_user_main_fn != null { + g_user_main_fn(); + } + null; +} + +// Bring up EGL on g_app_window. Sets g_egl_display / g_egl_context / +// g_egl_surface and makes the context current. Returns false on any +// failure — caller bails on the render thread. +sx_android_egl_init :: () -> bool { + g_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + if g_egl_display == EGL_NO_DISPLAY { return false; } + + major : s32 = 0; + minor : s32 = 0; + if eglInitialize(g_egl_display, @major, @minor) == EGL_FALSE { return false; } + + cfg_attrs : [13]s32 = .{ + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_DEPTH_SIZE, 24, + EGL_NONE, + }; + num_cfg : s32 = 0; + if eglChooseConfig(g_egl_display, @cfg_attrs[0], @g_egl_config, 1, @num_cfg) == EGL_FALSE { return false; } + if num_cfg < 1 { return false; } + + visual_id : s32 = 0; + eglGetConfigAttrib(g_egl_display, g_egl_config, EGL_NATIVE_VISUAL_ID, @visual_id); + ANativeWindow_setBuffersGeometry(g_app_window, 0, 0, visual_id); + + ctx_attrs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + g_egl_context = eglCreateContext(g_egl_display, g_egl_config, EGL_NO_CONTEXT, @ctx_attrs[0]); + if g_egl_context == EGL_NO_CONTEXT { return false; } + + g_egl_surface = eglCreateWindowSurface(g_egl_display, g_egl_config, g_app_window, null); + if g_egl_surface == EGL_NO_SURFACE { return false; } + + if eglMakeCurrent(g_egl_display, g_egl_surface, g_egl_surface, g_egl_context) == EGL_FALSE { return false; } + + g_viewport_w = ANativeWindow_getWidth(g_app_window); + g_viewport_h = ANativeWindow_getHeight(g_app_window); + true; +} + +// ── Touch event queue ─────────────────────────────────────────────────── + +sx_android_push_touch :: (action: s32, x: f32, y: f32) { + sx_android_ensure_touch_mutex(); + pthread_mutex_lock(xx @g_touch_mutex_storage[0]); + next := (g_touch_tail + 1) % 64; + if next != g_touch_head { // drop on full + g_touch_queue[g_touch_tail] = TouchEvent.{ action = action, x = x, y = y }; + g_touch_tail = next; + } + pthread_mutex_unlock(xx @g_touch_mutex_storage[0]); +} + +sx_android_drain_touches :: (out: *List(Event)) { + sx_android_ensure_touch_mutex(); + pthread_mutex_lock(xx @g_touch_mutex_storage[0]); + while g_touch_head != g_touch_tail { + t := g_touch_queue[g_touch_head]; + g_touch_head = (g_touch_head + 1) % 64; + // MotionEvent actions: 0=DOWN, 1=UP, 2=MOVE. Map onto chess's + // existing mouse Event variants — touch becomes a left-button + // mouse on the same screen coords; delta unused on Android. + pos : Point = .{ x = t.x, y = t.y }; + if t.action == 0 { + out.append(.mouse_down(.{ position = pos, button = .left })); + } else if t.action == 1 { + out.append(.mouse_up(.{ position = pos, button = .left })); + } else if t.action == 2 { + out.append(.mouse_moved(.{ position = pos, delta = .{ x = 0, y = 0 } })); + } + } + pthread_mutex_unlock(xx @g_touch_mutex_storage[0]); +} + +sx_android_ensure_touch_mutex :: () { + if g_touch_mutex_inited { return; } + pthread_mutex_init(xx @g_touch_mutex_storage[0], null); + g_touch_mutex_inited = true; +} + +// ── AndroidPlatform ───────────────────────────────────────────────────── + +AndroidPlatform :: struct { + title: [:0]u8 = ""; + width: s32 = 0; + height: s32 = 0; + events: List(Event) = .{}; +} + +impl Platform for AndroidPlatform { + init :: (self: *AndroidPlatform, title: [:0]u8, w: s32, h: s32) -> bool { + self.title = title; + self.width = w; + self.height = h; + true; + } + + begin_frame :: (self: *AndroidPlatform) -> FrameContext { + FrameContext.{ + viewport_w = xx g_viewport_w, + viewport_h = xx g_viewport_h, + pixel_w = g_viewport_w, + pixel_h = g_viewport_h, + dpi_scale = g_dpi_scale, + delta_time = 0.016, + target_present_time = 0.0, + }; + } + + end_frame :: (self: *AndroidPlatform) { + if g_egl_display != null and g_egl_surface != null { + eglSwapBuffers(g_egl_display, g_egl_surface); + } + } + + poll_events :: (self: *AndroidPlatform) -> []Event { + self.events.len = 0; + sx_android_drain_touches(@self.events); + result : []Event = ---; + result.ptr = self.events.items; + result.len = self.events.len; + result; + } + + safe_insets :: (self: *AndroidPlatform) -> EdgeInsets { + EdgeInsets.{}; + } + + keyboard :: (self: *AndroidPlatform) -> KeyboardState { + KeyboardState.zero(); + } + show_keyboard :: (self: *AndroidPlatform) { } + hide_keyboard :: (self: *AndroidPlatform) { } + + run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) { + g_frame_fn = frame_fn; + g_frame_fn_set = true; + // `frame_fn` is expected to call `g_plat.end_frame()` which does + // the `eglSwapBuffers` — don't swap again here or the back buffer + // is presented twice per render, alternating with the previous + // frame's contents → visible flicker. + while !g_should_stop { + frame_fn(); + usleep(1000); + } + } + + stop :: (self: *AndroidPlatform) { + g_should_stop = true; + } + + shutdown :: (self: *AndroidPlatform) { + if g_egl_display != null { + eglMakeCurrent(g_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if g_egl_surface != null { eglDestroySurface(g_egl_display, g_egl_surface); g_egl_surface = null; } + if g_egl_context != null { eglDestroyContext(g_egl_display, g_egl_context); g_egl_context = null; } + eglTerminate(g_egl_display); + g_egl_display = null; + } + } +} diff --git a/src/ir/jni_java_emit.zig b/src/ir/jni_java_emit.zig index df50306..e8b2eae 100644 --- a/src/ir/jni_java_emit.zig +++ b/src/ir/jni_java_emit.zig @@ -180,8 +180,11 @@ fn appendDotted( buf: *std.ArrayList(u8), slash_path: []const u8, ) EmitError!void { + // `/` and `$` both become `.` in Java source: `android/view/SurfaceHolder$Callback` + // → `android.view.SurfaceHolder.Callback`. The `$` form is the JNI-descriptor + // / class-file shape for nested classes; Java source uses `.` for both. for (slash_path) |c| { - try buf.append(allocator, if (c == '/') '.' else c); + try buf.append(allocator, if (c == '/' or c == '$') '.' else c); } } @@ -210,7 +213,13 @@ fn emitOverride( try buf.appendSlice(allocator, md.name); try buf.append(allocator, '('); try emitJavaParamList(allocator, buf, md, opts); - try buf.appendSlice(allocator, ") {\n sx_"); + // Non-void return types `return` the native delegate's result; void + // returns just call it. The user's sx-side body decides what to + // return — the Java side is a pass-through. + const has_ret = md.return_type != null; + try buf.appendSlice(allocator, ") {\n "); + if (has_ret) try buf.appendSlice(allocator, "return "); + try buf.appendSlice(allocator, "sx_"); try buf.appendSlice(allocator, md.name); try buf.append(allocator, '('); try emitJavaArgList(allocator, buf, md); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 369d7cd..1ab3bed 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -334,7 +334,7 @@ pub const Lowering = struct { /// This preserves the old behavior for comptime evaluation contexts. pub fn lowerDecls(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { - self.current_source_file = decl.source_file; + self.setCurrentSourceFile(decl.source_file); const is_imported = if (self.main_file) |mf| (if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false) else @@ -393,7 +393,7 @@ pub const Lowering = struct { /// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies. fn scanDecls(self: *Lowering, decls: []const *const Node) void { for (decls) |decl| { - self.current_source_file = decl.source_file; + self.setCurrentSourceFile(decl.source_file); const is_imported = if (self.main_file) |mf| (if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false) else @@ -478,7 +478,7 @@ pub const Lowering = struct { // Simple value constants with type annotation (e.g. AF_INET :s32: 2) if (cd.type_annotation != null) { switch (cd.value.data) { - .int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal => { + .int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal => { const ty = self.resolveType(cd.type_annotation); self.module_const_map.put(cd.name, .{ .value = cd.value, .ty = ty }) catch {}; }, @@ -778,7 +778,7 @@ pub const Lowering = struct { // Function not yet declared — create it fresh via lowerFunction self.lowerFunction(fd, name, false); // Restore builder state - self.current_source_file = saved_source_file; + self.setCurrentSourceFile(saved_source_file); self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.force_block_value = saved_force_block_value; @@ -792,10 +792,10 @@ pub const Lowering = struct { // Re-use the existing function slot — switch builder to it self.builder.func = fid; const func = &self.module.functions.items[@intFromEnum(fid)]; - self.current_source_file = func.source_file; + self.setCurrentSourceFile(func.source_file); if (!func.is_extern) { // Already promoted (e.g., via lowerComptimeDeps) — skip - self.current_source_file = saved_source_file; + self.setCurrentSourceFile(saved_source_file); self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; @@ -864,7 +864,7 @@ pub const Lowering = struct { } // Restore builder state - self.current_source_file = saved_source_file; + self.setCurrentSourceFile(saved_source_file); self.scope = saved_scope; self.func_defer_base = saved_defer_base; self.block_terminated = saved_block_terminated; @@ -1811,7 +1811,7 @@ pub const Lowering = struct { .call => |c| self.lowerCall(&c), .ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic), .field_access => |fa| self.lowerFieldAccess(&fa, node.span), - .struct_literal => |sl| self.lowerStructLiteral(&sl), + .struct_literal => |sl| self.lowerStructLiteral(&sl, node.span), .array_literal => |al| self.lowerArrayLiteral(&al), .index_expr => |ie| self.lowerIndexExpr(&ie), .slice_expr => |se| self.lowerSliceExpr(&se), @@ -2883,7 +2883,7 @@ pub const Lowering = struct { // ── Struct/enum/union ops ─────────────────────────────────────── - fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral) Ref { + fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref { // Check for tagged enum construction: .Variant.{ payload_fields } // This happens when type_expr is an enum_literal and target_type is a union if (sl.type_expr) |te| { @@ -2893,7 +2893,38 @@ pub const Lowering = struct { if (!union_ty.isBuiltin()) { const union_info = self.module.types.get(union_ty); if (union_info == .tagged_union) { - return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union); + return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span); + } + } + } + } + + // `.{ name = ... }` against a tagged-union target_type. Reject: + // the only valid construction forms are `.variant(payload)` and + // `.variant.{ field, ... }`. Falling through would lower the + // user's values straight into the `(tag, payload_bytes)` slot + // pair and emit IR that LLVM later rejects. + if (sl.type_expr == null and sl.struct_name == null) { + const tu_ty = self.target_type orelse .s64; + if (!tu_ty.isBuiltin()) { + const tu_info = self.module.types.get(tu_ty); + if (tu_info == .tagged_union) { + if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) { + const first_name = sl.field_inits[0].name.?; + if (self.diagnostics) |diags| { + const ty_name = self.formatTypeName(tu_ty); + if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) { + diags.addFmt( + .err, + span, + "cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`", + .{ ty_name, first_name, first_name, first_name }, + ); + } else { + self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span); + } + } + return self.builder.enumInit(0, Ref.none, tu_ty); } } } @@ -3459,7 +3490,13 @@ pub const Lowering = struct { variant_name: []const u8, union_ty: TypeId, union_info: types.TypeInfo.TaggedUnionInfo, + span: ast.Span, ) Ref { + if (self.findTaggedVariant(union_info, variant_name) == null) { + self.emitBadVariant(union_ty, union_info, variant_name, span); + return self.builder.enumInit(0, Ref.none, union_ty); + } + const tag = self.resolveVariantValue(union_ty, variant_name); const name_id = self.module.types.internString(variant_name); @@ -3512,6 +3549,40 @@ pub const Lowering = struct { return self.builder.enumInit(tag, payload, union_ty); } + fn findTaggedVariant( + self: *Lowering, + union_info: types.TypeInfo.TaggedUnionInfo, + variant_name: []const u8, + ) ?usize { + const name_id = self.module.types.internString(variant_name); + for (union_info.fields, 0..) |f, i| { + if (f.name == name_id) return i; + } + return null; + } + + fn emitBadVariant( + self: *Lowering, + union_ty: TypeId, + union_info: types.TypeInfo.TaggedUnionInfo, + variant_name: []const u8, + span: ast.Span, + ) void { + const diags = self.diagnostics orelse return; + const ty_name = self.formatTypeName(union_ty); + var list: std.ArrayList(u8) = .empty; + for (union_info.fields, 0..) |f, i| { + if (i > 0) list.appendSlice(self.alloc, ", ") catch return; + list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return; + } + diags.addFmt( + .err, + span, + "'{s}' is not a variant of '{s}' (variants are: {s})", + .{ variant_name, ty_name, list.items }, + ); + } + /// Resolve a variant name to its runtime value (flags: power-of-2, regular: index). fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 { if (ty.isBuiltin()) return 0; @@ -4144,7 +4215,19 @@ pub const Lowering = struct { return Ref.none; }; - const ret_ty = self.module.types.ptrTo(.void); // jobject + // sx-side return type is `*Self` — resolve to a pointer to the + // foreign-class struct type so method dispatch on the new + // jobject works (`view := SurfaceView.new(ctx); view.getHolder()`). + // At LLVM level still ptr; the sx type table is what method + // resolution consults. + const self_struct_name = self.module.types.internString(fcd.name); + const self_struct_id = if (self.module.types.findByName(self_struct_name)) |existing| + existing + else blk: { + const info: types.TypeInfo = .{ .@"struct" = .{ .name = self_struct_name, .fields = &.{} } }; + break :blk self.module.types.intern(info); + }; + const ret_ty = self.module.types.ptrTo(self_struct_id); const name_sid = self.module.types.internString(""); const name_ref = self.builder.constString(name_sid); @@ -9703,6 +9786,7 @@ pub const Lowering = struct { return self.builder.constString(sid); }, .undef_literal => return self.builder.constUndef(ci.ty), + .null_literal => return self.builder.constNull(ci.ty), else => { // Complex expressions (struct_literal, call, etc.) — lower on demand const saved_target = self.target_type; @@ -9730,6 +9814,14 @@ pub const Lowering = struct { return self.module.types.findByName(name_id) != null; } + /// Update `self.current_source_file` and mirror it onto `diags.current_source_file`, + /// so any diagnostic emitted from inside a function lowered from another module is + /// attributed to that module — not whichever file the diagnostics list was init'd with. + fn setCurrentSourceFile(self: *Lowering, source_file: ?[]const u8) void { + self.current_source_file = source_file; + if (self.diagnostics) |d| d.current_source_file = source_file; + } + fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref { if (self.diagnostics) |diags| { diags.addFmt(.err, span, "unresolved: '{s}'", .{name}); @@ -10115,13 +10207,13 @@ pub const Lowering = struct { } }; -/// JNI map: pointer types collapse to `*void` (jobject opaque handle); -/// primitives pass through unchanged. +/// JNI param/return type resolution: user-declared types pass through +/// `resolveType` so the method body can dispatch on richer foreign-class +/// types (`holder.getSurface()` etc.). At LLVM level both `*SurfaceHolder` +/// and `*void` lower to the same `ptr`, so the C ABI shape Java sees is +/// unchanged — only sx-side method resolution benefits. fn jniMapParamType(self: *Lowering, type_node: *ast.Node) TypeId { - return switch (type_node.data) { - .pointer_type_expr => self.module.types.ptrTo(.void), - else => self.resolveType(type_node), - }; + return self.resolveType(type_node); } /// Encode a (foreign_path, method_name) pair as the JNI-resolved symbol diff --git a/src/target.zig b/src/target.zig index 121dd17..08ab1b7 100644 --- a/src/target.zig +++ b/src/target.zig @@ -533,6 +533,10 @@ fn buildJniMainManifest(allocator: std.mem.Allocator, package: []const u8, lib_n try class_name.append(allocator, if (ch == '/') '.' else ch); } const activity_name = try class_name.toOwnedSlice(allocator); + // `Theme.DeviceDefault.NoActionBar.Fullscreen` removes both the + // ActionBar title (the "sxchess" strip) and the status bar — sx-rendered + // apps own the whole window. Consumers wanting a different theme will + // ship their own manifest via `--manifest`. return std.fmt.allocPrint(allocator, \\ \\ \\ \\