iOS lock step keyboard + metal

This commit is contained in:
agra
2026-05-18 17:40:10 +03:00
parent b43472e6ab
commit f9ecf9d00e
68 changed files with 4794 additions and 203 deletions

View File

@@ -0,0 +1,32 @@
// Sub-32-bit enum variants ride through a protocol-typed receiver's
// method call without being collapsed to tag=0.
#import "modules/std.sx";
Fmt :: enum { a; b; }
Proto :: protocol {
take_fmt :: (f: Fmt);
}
Impl :: struct {}
impl Proto for Impl {
take_fmt :: (self: *Impl, f: Fmt) {
n : s64 = xx f;
print("proto f = {}\n", n);
}
}
take :: (f: Fmt) -> s64 {
n : s64 = xx f;
n;
}
main :: () -> s32 {
print("direct a={} b={}\n", take(.a), take(.b));
p : Proto = xx @Impl.{};
p.take_fmt(.b);
p.take_fmt(.a);
0;
}

View File

@@ -0,0 +1,20 @@
// A closure stored in a struct field receives sub-32-bit enum args
// with the right tag, same as direct or protocol-dispatched calls.
#import "modules/std.sx";
Fmt :: enum { a; b; }
Ctx :: struct {
on: Closure(Fmt) -> void;
}
main :: () -> s32 {
c : Ctx = .{ on = (f: Fmt) => {
n : s64 = xx f;
print("cl f = {}\n", n);
}};
c.on(.b);
c.on(.a);
0;
}

View File

@@ -0,0 +1,30 @@
// A protocol method declared with a real pointer return (`-> *u8`,
// NOT `-> Self`) returns the raw pointer to the caller without the
// dispatch path auto-dereferencing it. Without this, a method whose
// pointee is a single byte gets `sizeof(target)` bytes loaded past
// it and segfaults.
#import "modules/std.sx";
Proto :: protocol {
get :: () -> *u8;
}
Impl :: struct {
val: u8 = 42;
}
impl Proto for Impl {
get :: (self: *Impl) -> *u8 {
@self.val;
}
}
main :: () -> s32 {
imp : Impl = .{};
p : Proto = xx @imp;
raw : *u8 = p.get();
addr_word : u64 = xx raw;
print("got pointer: {}\n", addr_word != 0);
0;
}

View File

@@ -0,0 +1,20 @@
// Phase 1 (xx-via-Into mechanism): proves the new syntax parses + lowers
// without error. The parameterised protocol Into(Target: Type) and the
// matching `impl Into(Block) for Closure() -> void` declarations are
// registered but unused. Resolution (Phase 3) is what makes the impl
// reachable from `xx`.
#import "modules/std.sx";
MyTag :: struct { value: s64 = 0; }
impl Into(MyTag) for s64 {
convert :: (self: s64) -> MyTag {
.{ value = self };
}
}
main :: () -> s32 {
print("ok\n");
0;
}

View File

@@ -0,0 +1,20 @@
// Phase 3 (xx-via-Into): a user-defined `impl Into(Target) for Source`
// reaches the xx operator through compile-time dispatch. The compiler
// monomorphises `convert` for the (Source, Target) pair and emits a
// direct call — no vtable, identical to a hand-written call.
#import "modules/std.sx";
MyString :: struct { tag: s64 = 0; }
impl Into(MyString) for s64 {
convert :: (self: s64) -> MyString {
.{ tag = self };
}
}
main :: () -> s32 {
x : MyString = xx 42;
print("tag = {}\n", x.tag);
0;
}

View File

@@ -0,0 +1,12 @@
// Helper for 93-into-import-scope.sx: declares Wrap + an impl Into for it.
// No paired tests/expected file — not executed standalone by the runner.
#import "modules/std.sx";
Wrap :: struct { v: s64 = 0; }
impl Into(Wrap) for s64 {
convert :: (self: s64) -> Wrap {
.{ v = self * 10 };
}
}

View File

@@ -0,0 +1,13 @@
// Phase 4 (xx-via-Into mechanism): an `impl Into(...) for ...` lives in
// a separate file and reaches the xx site through a direct `#import`.
// The visibility filter accepts the impl because the user file
// transitively imports the impl's defining module.
#import "modules/std.sx";
#import "./93-into-impl-helper.sx";
main :: () -> s32 {
w : Wrap = xx 3;
print("w.v = {}\n", w.v);
0;
}

View File

@@ -0,0 +1,14 @@
// Extern data globals via `<name> : <type> #foreign;`. Lets sx code
// reference libSystem / framework symbols (NSConcreteStackBlock,
// __stdinp, etc.) for FFI bridges. Mirrors the long-standing
// `<fn> :: (...) -> ... #foreign;` form on the function side.
#import "modules/std.sx";
__stdinp : *void #foreign;
main :: () -> s32 {
addr_bits : u64 = xx @__stdinp;
print("stdin extern global non-null: {}\n", addr_bits != 0);
0;
}

View File

@@ -0,0 +1,17 @@
// `xx <closure> : Block` builds an Apple-ABI block whose invoke
// trampoline delegates to the sx closure. Verifies end-to-end:
// stdlib Block layout, _NSConcreteStackBlock extern, per-signature
// invoke trampoline, Into(Block) for Closure() -> void. Runs on
// macOS — invokes the block's invoke fn directly via a typed fn
// pointer instead of going through the Obj-C runtime.
#import "modules/std.sx";
#import "modules/std/objc_block.sx";
main :: () -> s32 {
cl := () => { print("noop block ran\n"); };
b : Block = xx cl;
invoke_fn : (*Block) -> void = xx b.invoke;
invoke_fn(@b);
0;
}

View File

@@ -0,0 +1,17 @@
// A capturing closure rides through `xx ... : Block` and the
// captured state survives across the call. The block's sx_env field
// holds the closure's env pointer; the invoke trampoline restores it
// before delegating.
#import "modules/std.sx";
#import "modules/std/objc_block.sx";
main :: () -> s32 {
x : s64 = 42;
y : s64 = 100;
cl := () => { print("x + y = {}\n", x + y); };
b : Block = xx cl;
invoke_fn : (*Block) -> void = xx b.invoke;
invoke_fn(@b);
0;
}

View File

@@ -0,0 +1,19 @@
// `xx <closure>` passed as a `*Block` fn argument auto-allocates the
// Block instance and passes its address — no named temp required.
// Matches the ergonomics of ObjC's `^{...}` literal at the call site.
#import "modules/std.sx";
#import "modules/std/objc_block.sx";
invoke_once :: (b: *Block) {
invoke_fn : (*Block) -> void = xx b.invoke;
invoke_fn(b);
}
main :: () -> s32 {
x : s64 = 7;
invoke_once(xx () => {
print("inline block, x = {}\n", x);
});
0;
}

View File

@@ -1,68 +0,0 @@
// issue-0026: Chess game on iOS-sim with `plat.gpu_mode = .metal` crashes
// inside `[MTLTexture replaceRegion:mipmapLevel:withBytes:bytesPerRow:]`
// when uploading the 1024×1024 R8 font atlas. The 1×1 RGBA8 white tex
// through the SAME code path (metal_update_texture_region_ios in
// library/modules/gpu/metal.sx) works.
//
// Blocked on issue-0024 (NSLog inside if/else not firing — or unified-log
// buffer loss on crash; investigation pending) — without a trustworthy
// tracer we can't reliably bisect which arg arrives wrong. Most likely
// cause: this is downstream of issue-0025's ABI gaps (MTLRegion is 48
// bytes and goes through `xx objc_msgSend` cast, which is the
// call_indirect path that issue-0025 part B covers).
//
// ── Reproduction recipe ───────────────────────────────────────────────────
//
// cd /Users/agra/projects/game
// /Users/agra/projects/sx/zig-out/bin/sx build --target ios-sim main.sx \
// --bundle sx-out/ios/SxChess.app --bundle-id co.swipelab.sxchess \
// -F ~/Library/Frameworks
// cp -R assets sx-out/ios/SxChess.app/
// codesign --force --sign - --timestamp=none sx-out/ios/SxChess.app
// xcrun simctl install booted sx-out/ios/SxChess.app
// xcrun simctl launch --terminate-running-process booted co.swipelab.sxchess
// sleep 4 && xcrun simctl io booted screenshot /tmp/sx-chess.png
//
// Expected (after fix): chess board renders via Metal.
// Observed: app launches, returns immediately to home screen, no screen
// touched. The simpler examples/63-metal-clear.sx demo still renders the
// colored triangle on the same sim, so the Metal pipeline itself works
// for small uploads.
//
// ── Candidate root causes (in priority order) ─────────────────────────────
//
// 1. issue-0025 fallout (most likely): MTLRegion (48 B by value) passed
// via the *MTLRegion workaround. The call_indirect path (issue-0025
// part B) doesn't ABI-coerce, so the pointer-shaped declaration may
// not actually pass the address in the right register slot for that
// call site shape (6 args, including the indirect aggregate).
//
// 2. iOS-sim Metal-driver limitation: `setStorageMode:.shared` may not be
// honored for r8 textures of this size; default may be `.private`
// which precludes CPU-side replaceRegion. Workaround would be to
// upload via `MTLBuffer` + `MTLBlitCommandEncoder` (newBufferWithBytes
// + copyFromBuffer:sourceOffset:sourceBytesPerRow:...:toTexture:...).
//
// 3. sx-side `xx` cast bug: bytes_per_row : u64 = xx (u32_expr) may
// truncate or sign-extend incorrectly. Less likely (the math comes
// out to 1024, which fits in any width).
//
// ── How to resolve ────────────────────────────────────────────────────────
//
// After issues 0024 + 0025 are landed:
// 1. Re-add the trace NSLog markers ("[metal] U1..U5" in
// metal_update_texture_region_ios) — now they should actually print.
// 2. Re-build + relaunch chess on iOS-sim.
// 3. If U5 fires after U4 (no crash inside msg_replace), the bug was
// ABI-related; declare success and rename this file to
// examples/NN-metal-large-region-upload.sx (next free NN).
// 4. If U4 → crash persists, fall back to the MTLBuffer + blit
// encoder path in metal.sx's create_texture (when pixels != null,
// allocate a temporary MTLBuffer with newBufferWithBytes:length:options:
// then run a one-shot command buffer with a MTLBlitCommandEncoder
// copying the buffer into the texture). This is the Apple-recommended
// approach for large texture initial-uploads.
#import "modules/std.sx";
main :: () -> s32 { 0; }

View File

@@ -1,50 +0,0 @@
// issue-0027: Feature — support Obj-C blocks (^{...}) so sx code can call
// APIs that take a block parameter. Required for step 4 of the Metal port
// (keyboard lockstep via `[UIView animateWithDuration:animations:^{...}]`),
// and broadly useful for any UIKit/AppKit API.
//
// ── Proposed surface ──────────────────────────────────────────────────────
//
// Option A — comptime intrinsic that wraps a sx closure as a block:
//
// block := objc_block(@my_closure); // returns *void (an id<Block>)
// msg_block(view, sel, 0.3, block); // pass like any id arg
//
// Internals: emit a Block_literal struct constant with the right invoke
// fn pointer, isa, flags, descriptor pointer. Approximately what clang
// generates for ^{...}.
//
// Option B — surface-level syntax `^{ ... }` that lowers to Option A
// automatically. Cleaner for users; more parser work.
//
// Recommended: start with Option A (intrinsic). Migrate to Option B once
// the codegen path is proven.
//
// ── Implementation sketch ────────────────────────────────────────────────
//
// 1. New `library/modules/std/objc_block.sx` defining the Block_literal
// struct that mirrors clang's layout (isa, flags, reserved, invoke fn
// pointer, descriptor pointer).
// 2. `objc_block(fn_or_closure) -> *void` intrinsic that builds the
// literal at the call site. Initial implementation can be a
// stack-allocated block (_NSConcreteStackBlock); upgrade to
// heap-promoted (_Block_copy) once block lifetime exceeds the call.
// 3. Link libSystem's symbols `_NSConcreteStackBlock` and
// `_NSConcreteGlobalBlock` (auto on iOS; may need `#library "System"`
// on macOS).
// 4. (Deferred) surface syntax `^{ ... }` — parser hook + lowering
// to the intrinsic. Must not clash with bitwise XOR `^`.
//
// ── References ────────────────────────────────────────────────────────────
//
// - Apple block ABI spec (clang's "Block Implementation Specification")
// - _NSConcreteStackBlock + _NSConcreteGlobalBlock from libSystem
//
// ── Real-world impact ─────────────────────────────────────────────────────
//
// Without this, the keyboard inset cannot be animated in lockstep with the
// keyboard slide. See library/modules/platform/uikit.sx's
// uikit_keyboard_will_change_frame comments for the deferred lockstep work.
#import "modules/std.sx";
main :: () -> s32 { 0; }

17
examples/issue-0032.sx Normal file
View File

@@ -0,0 +1,17 @@
// Phase 2 verification: two impls for the same parameterised-protocol
// (Source, Target) pair declared in the same file MUST produce a clean
// "duplicate impl" diagnostic at registration time.
#import "modules/std.sx";
MyA :: struct { v: s64 = 0; }
impl Into(MyA) for s64 {
convert :: (self: s64) -> MyA { .{ v = self }; }
}
impl Into(MyA) for s64 {
convert :: (self: s64) -> MyA { .{ v = self * 2 }; }
}
main :: () -> s32 { 0; }

View File

@@ -0,0 +1,10 @@
// Helper that defines the impl. issue-0033's user file does NOT
// directly import this — that's the whole point of the test.
#import "modules/std.sx";
#import "./issue-0033-types.sx";
impl Into(Wrap) for s64 {
convert :: (self: s64) -> Wrap {
.{ v = self * 10 };
}
}

View File

@@ -0,0 +1,2 @@
// Shared type for issue-0033 — Wrap struct.
Wrap :: struct { v: s64 = 0; }

View File

@@ -0,0 +1,10 @@
// User file uses xx but only imports the shared types — NOT the impl.
// The Phase 4 visibility filter should reject the impl from issue-0033-impl.sx.
#import "modules/std.sx";
#import "./issue-0033-types.sx";
run_user :: () -> s32 {
w : Wrap = xx 7;
print("user: w.v = {}\n", w.v);
0;
}

16
examples/issue-0033.sx Normal file
View File

@@ -0,0 +1,16 @@
// Phase 4 verification: an `impl Into(...) for ...` is registered into the
// global impl table when its module is imported anywhere in the program, but
// is only **visible** from files that themselves transitively import the impl's
// defining module. Here:
// - issue-0033-impl.sx declares an `impl Into(Wrap) for s64`.
// - issue-0033-user.sx tries to `xx 7 : Wrap` but only imports the shared
// types — not the impl module.
// - The xx at issue-0033-user.sx:7 must produce a clean "no visible xx
// conversion" diagnostic, not silently fall through to whatever was
// registered in another module.
#import "modules/std.sx";
#import "./issue-0033-impl.sx";
#import "./issue-0033-user.sx";
main :: () -> s32 { run_user(); }

View File

@@ -0,0 +1,9 @@
// Helper A — one of two conflicting impls for the same (s64, Wrap) pair.
#import "modules/std.sx";
#import "./issue-0034-types.sx";
impl Into(Wrap) for s64 {
convert :: (self: s64) -> Wrap {
.{ v = self * 10 };
}
}

View File

@@ -0,0 +1,9 @@
// Helper B — second conflicting impl for the same (s64, Wrap) pair.
#import "modules/std.sx";
#import "./issue-0034-types.sx";
impl Into(Wrap) for s64 {
convert :: (self: s64) -> Wrap {
.{ v = self + 100 };
}
}

View File

@@ -0,0 +1,2 @@
// Shared type for issue-0034.
Wrap :: struct { v: s64 = 0; }

14
examples/issue-0034.sx Normal file
View File

@@ -0,0 +1,14 @@
// Phase 5 verification: two impls for the same (Source, Target) pair are
// both visible from the same xx site (because both their defining modules
// are transitively imported). The compiler must emit a clean
// "duplicate xx conversion" diagnostic naming both modules.
#import "modules/std.sx";
#import "./issue-0034-impl-a.sx";
#import "./issue-0034-impl-b.sx";
main :: () -> s32 {
w : Wrap = xx 7;
print("w.v = {}\n", w.v);
0;
}