76e0e97bfaed102732c6cbccd2a185fd837a0686
77 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b31fbae757 |
platform/sdl3: chdir to .app bundle on macOS so CWD-relative assets resolve
A macOS .app launched with CWD=/ (Finder/open) could not find CWD-relative
assets (read_file_bytes("assets/...")) and crashed in stbtt with a null font.
SdlPlatform.init now chdirs to SDL_GetBasePath() when running from inside a
.app bundle (detected by ".app" in the base path), mirroring uikit.sx s iOS
chdir_to_bundle. Gated so the sx run dev flow (binary not bundled) keeps the
project CWD. Verified: direct-exec with CWD=/ now stays alive (was: instant
stbtt segfault). Filed issue 0051 with the analysis.
Note: launching via Finder/open additionally triggers Gatekeeper App
Translocation for the dev-signed bundle (separate code-signing concern, not
the asset path).
|
||
|
|
3ac13b7442 |
ir: Type as first-class value (Any-shaped {tag, value})
Previously, `t : Type = f64` stored a boxed string carrying the literal
name "f64"; comparisons and `type_of`/`type_name` round-trips lost the
underlying TypeId. This switches `Type` to a runtime-representable Any
pair: `{ tag = .any.index() (meta-marker), value = TypeId.index() }`.
Mechanism:
- `const_type` emits a 16-byte Any aggregate via insertvalue.
- `TypeId.any` advertises 16 bytes / 8-byte alignment so structs that
embed `t: Type` size correctly under verifySizes.
- `lowerBinaryOp` folds `==`/`!=` between static type-refs to a
`const_bool`, and decomposes runtime Any-vs-Any compares via
`unbox_any` so LLVM doesn't see icmp on aggregates.
- `lowerMatch`'s `is_type_match` path unboxes Any-typed subjects to
the i64 type tag before the switch, so `case type:` etc. fire.
- `lowerRuntimeDispatchCall` (used by `case T: ... cast(t) val`) does
the same unbox for the type-tag arg.
- `type_of(val: Any)` rebuilds an Any with `{.any, tag_of(val)}` so
the result is itself a `Type` value, not a bare i64.
- `buildPackSliceValue` stops re-boxing const_type — the value is
already canonical Any.
- `__sx_type_names` now indexes by TypeId across the whole table
using the new `types.formatTypeName` (structural names for `*T`,
`[]T`, `[N]T`, `?T`, `Vector(N,T)`, function/closure/tuple) so
runtime `type_name(t)` works for compound types.
- `interp.zig`'s comptime `type_name` accepts either the bare
`.type_tag` Value or the Any-boxed aggregate it now sees.
- `scanDecls` registers `Vec4 :: Vector(4, f32)` style aliases in
`type_alias_map` (before the `fn_ast_map` check; `Vector` IS a
`#builtin` fn). Lets `Vec4` in expression position lower as
`const_type(<vector tid>)`.
- `isStaticTypeArg` becomes scope-aware: a name shadowed by a runtime
local is not static. `isStaticTypeRef` is the symmetric helper for
the eq fold.
- `inferExprType` returns `.any` for bare type names (identifier and
type_expr) so pack arg types are correct.
Side effect: `print("{}", Vec4)` now prints the structural name
`Vector(4,f32)` rather than the alias literal `Vec4` — 12-meta's
expectation updated. Aliases stay pointer-equal to their target
(`Vec4 == Vector(4, f32)` is true).
Tests:
- examples/189-type-all-interactions.sx: 12-section comprehensive
coverage — literal `==`, `type_of(value) == T`, `Type` var storage,
`type_name` (static + runtime), printing Type values, generic
dispatch via `$T: Type`, `identity($T, val)`, `Wrap($T)`, reflection
builtins (`size_of`, `align_of`, `field_count`, `type_eq`),
`..$args` pack walking, `Type` in struct field, compound type
literals (`*Point`, `[4]s32`, `[]bool`, `?f64`).
- examples/12-meta.sx: expected output updated to reflect structural
name for the Vec4 alias path.
- ffi-objc-call-06-sret-return.ir: regenerated to absorb the new
type-name strings now emitted globally.
223/223 examples pass.
|
||
|
|
11eef8a6b1 |
ffi step 6: print / format migrate to ..\$args (comptime per-position pack)
`format` and `print` move from `..args: []Any` to `..$args`. The
pack-fn machinery monomorphises each call shape, so the
build_format-emitted body's `any_to_string(args[i])` substitutes
to the i-th concrete-typed call arg via packArgNodeAt — no more
runtime Any-boxing for static args. The Any boxing path still
fires for arg positions whose types collapse to `.any` (already
Any-typed inputs).
Net effect:
- Calls with statically-typed args produce per-shape monos
(`print__ct_<fmt_hash>__pack_s64_string_bool` etc). The mono
cache key now reflects both the format string AND the arg
types, so different shapes get distinct emit paths.
- Compile-time arity errors are now possible (callers passing
the wrong number of args mismatch the mono's positional
binding instead of silently mis-boxing).
- Optionals flow through the new `case optional:` arm in
`any_to_string` (commit
|
||
|
|
ce77867566 |
ffi any_to_string handles optionals — make-green
Closes the optional-through-Any gap that test 178 pinned. Stdlib (`library/modules/std.sx`): - New `optional_to_string :: (o: $T) -> string` returns `"null"` when the optional is None, otherwise recurses through `any_to_string` on the unwrapped inner value. Per-shape monomorphisation re-emits this for each concrete `?T`. - `any_to_string` grows a `case optional:` arm that dispatches through `cast(type) val` (same shape as `case struct:` etc.). The cast picks up the dynamic optional type from the Any tag. Compiler (`src/ir/lower.zig`): - `resolveTypeCategoryTags` recognises "optional" as a dynamic category, scanning the TypeTable for `info == .optional`. The type-switch dispatch then routes any ?T tag into the optional arm. IR snapshots regenerated where the optional addition shifted constant pool / string numbering: 142, ffi-objc-call-06, ffi-objc-dsl-07. 218/218 (test 178 included). The variadic auto-unwrap in `packVariadicCallArgs` stays in place — direct `print(opt)` calls still flow through it. The new arm closes the gap for struct fields, slice elements, and any other path that boxes an optional before stringifying. |
||
|
|
2eaf932fcf |
ffi M5.A.next.5.3: delete hand-rolled __block_invoke trampolines
Removes `__block_invoke_void` / `__block_invoke_bool` and their companion `Into(Block)` impls from `library/modules/std/objc_block.sx`. The generic `Into(Block) for Closure(..$args) -> $R` impl from step 5.2 now covers both shapes (and every other closure shape) via per-mono `#insert build_block_convert($args, $R)` source emission. Net stdlib shrinkage: ~52 lines, two trampolines + two per-shape impls down to zero. Adding a new block-shape consumer no longer requires touching stdlib — the impl emits per-call-shape on demand. `examples/95-objc-block-noop.sx` (zero-arg closure) and `examples/96-objc-block-multi-arg.sx` (user-declared per-shape impl for `Closure(s32, *void) -> void`) still pass: 95 routes through the new generic, 96 keeps its in-file impl as a documentation example of the user-declares-their-own path. Suite at 217/217. |
||
|
|
165b621ab3 |
ffi M5.A.next.5.2.B: generic Into(Block) impl — make-green
Adds the generic `impl Into(Block) for Closure(..$args) -> $R` in `library/modules/std/objc_block.sx` alongside the existing hand-rolled `Closure() -> void` and `Closure(bool) -> void` impls. The convert body is a single `#insert build_block_convert($args, $R);` — per-call-shape monomorphisation re-runs the builder so each closure shape gets its dedicated nested `callconv(.c)` trampoline + Block literal. The impl-mono path threads pack types through `pack_bindings[args]` and the single-type return through `type_bindings[R]`. Both need to be visible to the body's `$args` / `$R` expression-position references — the existing lowering only consulted `pack_arg_types` (set by pack-fn mono, not by tryPackImplMatch). Two small extensions: - `lowerExpr`'s `.comptime_pack_ref` arm now consults `pack_arg_types` → `pack_bindings` → `type_bindings` in order, treating a `type_bindings` hit as a single `const_type(T)` value rather than the slice form. - `resolveTypeArg` grows a `.comptime_pack_ref` arm that maps the same name through `type_bindings` so type-arg positions (e.g. inside `type_name(...)` in the builder body) resolve the bound single Type. - `type_bridge.isTypeShapedAstNode` lists `comptime_pack_ref` and `pack_index_type_expr` as type-shaped so `buildTypeBindings`'s strategy-1 explicit-arg path picks them up when calling a `$T: Type`-generic fn. `examples/177-generic-into-block.sx` flips green: a `Closure(s64, s64) -> void` (no hand-rolled impl) is converted through the generic impl, its block invoked via a typed `callconv(.c)` fn-pointer, and the closure's side effects land in the host globals. Hand-rolled impls remain for `()` and `(bool)` shapes; 5.3 deletes those once a focused test covers their behaviour through the generic path. Suite at 217/217. |
||
|
|
aeb950b86f |
ffi M5.A.next.5.1.B: build_block_convert added to stdlib — make-green
`build_block_convert(args: []Type, $ret: Type) -> string` emits
the convert-body source for the generic `Into(Block) for
Closure(..$args) -> $R` impl (step 5.2):
1. A nested `__invoke :: (block_self: *Block, arg0: T0, ...) ->
R callconv(.c) { ... }` trampoline matching the per-shape
Apple Block ABI.
2. A `return Block.{ ... };` literal whose `invoke` slot points
at the nested trampoline via `xx @__invoke`.
Void-returning shapes emit `typed_fn(block_self.sx_env, args...)`;
non-void emits `return typed_fn(...)`. Per-position arg names
follow `arg0`, `arg1`, ... in declaration order; the typed-fn
cast reconstructs the closure's call signature so the trampoline
hands control back to `sx_fn` with the right argument layout.
`examples/176-build-block-convert.sx` flips green (216/216).
|
||
|
|
5b3d86440b |
ffi: migrate remaining variadic decls to new ..name: []T form
Stdlib: - `format` / `print` in std.sx — both move from `args: ..Any` to `..args: []Any`. The post-issue-0049 lowering makes this safe across module boundaries. - `open` in fs.sx — `args: ..s32` → `..args: []s32`. Foreign C-variadic semantics are preserved (the trailing `, ...` lands in the generated `declare` regardless of which surface form is used). Examples: - `19-varargs.sx` — `sum` / `print_all` migrated. - `20-any-varargs.sx` — `print_any` / `count` migrated. - `50-smoke.sx` — `typed_sum` migrated. - `120-interp-variadic-any.sx` — comment-only update referencing the new form. - `ffi-foreign-cvariadic.sx` — three C-variadic foreign decls migrated; header comment refreshed. Suite stays at 214/214. The legacy `name: ..T` surface form is still accepted by the parser; rejection follows in a later commit once specs.md catches up. |
||
|
|
64dcbca06a |
ffi issue-0049: new-form variadic cross-module LLVM crash — xfail lock-in
Migrating stdlib's `path_join` to the new variadic syntax (`(..parts: []string) -> string`) surfaces a latent compiler bug: `resolveParamType` and `packVariadicCallArgs` treat the new-form declaration the same as the legacy `parts: ..string` and wrap the element type in `sliceOf` regardless of whether it already is one. The new form's `[]string` becomes `[][]string`; the call-site marshal pack emits `[N x string]` (correct) but the callee stores its slice param into a `[]([]string)`-typed slot. The shape mismatch propagates as null/undef Refs that crash `LLVMBuildExtractValue` inside `emitStrCmp` during emission. `examples/121-ios-sim-bundle.sx` (existing) and the new focused `examples/174-new-form-variadic-cross-module.sx` both fail today with the segfault. The next commit fixes `resolveParamType` + `packVariadicCallArgs` so both flip green. Stdlib's `format` / `print` / `open` and the example fixtures stay on the legacy form in this commit — they migrate in the follow-up cleanup commit. |
||
|
|
07f25689ff |
ffi M5.A revert: drop compiler synthesis, require explicit Into(Block) impls
Reconsidered the M5.A.2 cleanup. The compiler-synthesised trampoline
path was hidden behaviour — a user reading their code couldn't tell
how `xx my_closure : Block` worked without reading lower.zig. That's
exactly the kind of magic sx's design has been pushing against.
New design (strict mode):
1. Stdlib's modules/std/objc_block.sx hand-rolls
`__block_invoke_void` + `Into(Block) for Closure() -> void` and
the same pair for `Closure(bool) -> void` (restored from M5.A.2).
These are readable reference implementations of the bridge ABI.
2. The compiler intercept fires NO synthesis — instead, when
`tryUserConversion` can't find a reachable `Into(Block)` impl for
the closure's signature, it emits a focused diagnostic:
"no `Into(Block) for <Closure-sig>` impl — add a per-signature
`__block_invoke_<sig>` trampoline + Into impl alongside the
existing ones in modules/std/objc_block.sx, or declare it in
your own code"
3. Per-signature declarations live in stdlib (for common signatures)
or in user code (for app-specific ones). 96-objc-block-multi-arg
now demonstrates the user-side pattern in-file — it declares its
own `__block_invoke_void_s32_p` + `Into(Block) for Closure(s32,
*void) -> void` impl alongside its main().
Net effect:
- Every block bridge is source-visible. No hidden compiler magic.
- Users see exactly how the Apple ABI shape is constructed in sx
source — stdlib serves as the reference implementation.
- Compiler enforces the discipline: missing impl → clear diagnostic
pointing at the template.
- Coverage for arbitrary signatures requires conscious user opt-in,
not silent fallthrough.
Removed from lower.zig: `tryClosureToBlockConversion`,
`emitBlockInvokeTrampoline`, `mangleClosureSigForBlock`,
`mangleTypeForBlock`, and the `block_invoke_trampolines` dedup
state field. Net: the synthesis machinery is gone; only the
detection helper `isClosureToBlockCast` remains, used by the
diagnostic.
190/190 example tests pass; chess on iOS-sim green.
|
||
|
|
556e4e12ea |
ffi M5.A.2: drop hand-rolled __block_invoke_* impls + Into(Block) per-sig boilerplate
The compiler-synthesised trampoline path (previous commit) covers every closure signature on demand; the hand-rolled stdlib impls were only for two specific shapes (`Closure() -> void`, `Closure(bool) -> void`) and are now strictly redundant. Kept: the `Block` struct, `BlockDescriptor`, the `_NSConcreteStackBlock` extern decl, and the shared `__sx_block_descriptor` global. The compiler-emitted code references all four; users still need to `#import "modules/std/objc_block.sx";` to bring them into the module. Removed: `__block_invoke_void`, `__block_invoke_bool`, and both `impl Into(Block) for Closure(...) -> void` blocks. Replaced with a comment block explaining how the compiler now handles the cast. After this commit, `xx my_closure : Block` works for ANY closure signature with no per-signature stdlib boilerplate. 189/189 example tests pass; chess on iOS-sim green. |
||
|
|
5c1d00a877 |
ffi M4.B helpers: objcPropertyKind + ARC runtime decls + xfail tests
Three pieces, no behavior change yet:
1. `ObjcPropertyKind` enum (strong/weak/copy/assign) + `objcPropertyKind`
helper in lower.zig. Reads `field.property_modifiers`, applies the
default rule (`*<ObjC-class>` → strong; primitives → assign), and
emits loud diagnostics for the silent-error budget:
- unknown modifier name (typo) → "expected one of: strong, weak, copy, ..."
- conflicting modifiers (e.g. `strong,weak`) → "mutually exclusive"
- `weak` on non-object slot → "requires a pointer-to-Obj-C-class type"
- `copy` on non-object slot → same
- `strong` (default or explicit) on `*void` → "ambiguous: specify
#property(strong|weak|copy|assign) explicitly"
Called from `emitObjcDefinedClassPropertyImps` for validation; the
returned kind isn't wired into setter/getter/dealloc yet — that's
the next three commits.
2. `ensureArcRuntimeDecls` lazily declares libobjc's ARC helpers:
objc_retain, objc_release, objc_storeWeak, objc_loadWeakRetained,
objc_initWeak, objc_destroyWeak. Uses the existing
`ensureCRuntimeDecl` pattern; idempotent.
3. Fix existing NSObject method names in std/objc.sx — `isEqual_`,
`isKindOfClass_`, `respondsToSelector_` had trailing underscores
that the selector mangling turned into double-colon selectors
(`isEqual::`). Removed the trailing underscore so the selectors
come out as `isEqual:`, `isKindOfClass:`, `respondsToSelector:`
as Apple's runtime expects.
4. Two xfail regression tests:
- ffi-objc-arc-02-strong-property: assigns child to parent's strong
property, releases the original child reference. Midpoint check:
child's dealloc should NOT have fired (strong setter retained).
Pre-M4.B-setter: child dealloc fires immediately → "FAIL: child
dealloc'd at midpoint" snapshot. Exit code 1.
- ffi-objc-arc-03-weak-property: assigns target to holder's weak
property, releases target. Reads holder.target → should be null
(auto-niled). Pre-M4.B-getter/setter: reads stale pointer →
"FAIL: weak property didn't auto-nil" snapshot.
These will turn green as M4.B setter (commit 2), getter (commit 3),
and dealloc-cleanup (commit 4) land. Each subsequent commit updates
the snapshot to reflect the now-passing output.
189/189 example tests pass; chess on iOS-sim green.
|
||
|
|
29404afdee |
ffi M4.A: stdlib NSObject + autoreleasepool helper + extends rooting
Declare `NSObject` in std/objc.sx as `#foreign #objc_class("NSObject")`
with the canonical instance + class-method surface every Obj-C class
inherits: `retain`/`release`/`autorelease`/`new`/`alloc`/`init`/
`description`/`hash`/`isEqual_`/`isKindOfClass_`/`respondsToSelector_`/
`class`. Root the foreign-class hierarchy in uikit.sx at NSObject by
adding `#extends NSObject;` to every previously-unrooted declaration
(NSValue, NSNumber, NSDictionary, NSSet, NSNotification, NSBundle,
NSNotificationCenter, NSRunLoop, CADisplayLink, CALayer, EAGLContext,
UIScreen, UIResponder) plus deeper chain fixes (NSMutableDictionary
extends NSDictionary; UIWindow extends UIView; UIViewController
extends UIResponder). After this, M2.3's extends-chain walk finds
`retain`/`release` on any UIKit-typed value:
view := UIView.alloc().init();
defer view.release(); // canonical sx idiom — no language magic
Plus `autoreleasepool(body: Closure())` stdlib helper that wraps
`body` in `objc_autoreleasePoolPush` / `defer objc_autoreleasePoolPop`.
Required for Foundation factory returns; closure-call frame is real
cost so hot loops should inline the push/defer-pop pattern manually.
Smoke test `ffi-objc-arc-01-autoreleasepool.sx` exercises both
patterns; refresh of two IR snapshots picks up the new stdlib decls
appearing in test outputs that include `modules/std/objc.sx`.
185/185 example tests pass; chess on iOS-sim green.
|
||
|
|
9fbc24a602 |
ffi uikit cleanup: helpers → UIKitPlatform methods + declarative layerClass
Three threads, one commit because they're entangled:
1. Helper free functions on `*UIKitPlatform` (refresh_safe_insets,
read_screen_scale, create_gl_context, setup_renderbuffer,
present_renderbuffer, compute_layer_pixel_size) → methods on the
`impl Platform for UIKitPlatform` block. IMP-shape trampolines
(`uikit_keyboard_will_change_frame`, `uikit_scene_will_connect[_ios]`,
`uikit_gl_view_tick/layout/touches_*`, `uikit_subscribe_keyboard_notifications`)
also collapse into methods on UIKitPlatform — the
`(self: *void, _cmd: *void, ...)` form is no longer needed since
M3 made the #objc_class trampolines compiler-synthesized. Class
method bodies in SxAppDelegate / SxSceneDelegate / SxGLView /
SxMetalView now read `if g_uikit_plat == null { return; }
g_uikit_plat.x();` — no more `xx self, xx 0` casts at every IMP
call site.
2. Declarative `layerClass` form. SxGLView and SxMetalView promote
from the M2.1(a) constant-with-runtime-string-lookup workaround
(`layerClass :: *void = objc_getClass("CAEAGLLayer".ptr);`) to
the class-method expression-body form
(`layerClass :: () => CAEAGLLayer.class();`). Type stays `*void`
until M1.1.b lands `Class(T)` parameterisation; the value side
already matches the plan. Backing this: foreign-class declarations
for CAEAGLLayer (extended with `class :: () -> *void;`) and a new
CAMetalLayer foreign-class declaration alongside it. Both
`#extends CALayer` so the dispatch chain knows about the parent.
3. Optional-shape idiom pass on uikit.sx. `xx`-as-optional-wrap on
field assignments (`plat.gl_ctx = xx ctx`, `plat.text_field = xx tf`,
`plat.display_link = xx link`) dropped — implicit `T → ?T` does
the right thing. `!` force-unwraps replaced with `if val := opt
{ ... }` safe-narrowing (the keyboard handler, the GL-context
read in setup/present renderbuffer, the gl_view read in scene
bootstrap). `orelse` (Zig keyword) that briefly snuck into the
keyboard handler removed in favour of the `if win := plat.window`
narrowing pattern. Result: no `xx` casts left on the implicit
T→?T path; all optional access goes through `if val :=`.
IR snapshots `ffi-objc-call-06-sret-return.ir` and
`ffi-objc-dsl-07-mangling-table.ir` refresh to pick up the new
`object_getIvar` / `object_setIvar` runtime-helper declarations
introduced when M1.2 A.3 made instance-method bodies route field
access through the state ivar.
Chess on iOS-sim green throughout. 184/184 example tests pass.
|
||
|
|
f75923af00 |
uikit: type UIKitPlatform fields properly + handle optional in Obj-C encoding
The UIKitPlatform struct had a string of '*void = null; // UIWindow*' fields — the type lived in a comment, every callsite had to 'xx'-cast back to the real type. Migrated to the real foreign-class pointer types now that M3 declared all the relevant '#objc_class' aliases: window: ?*UIWindow root_vc: ?*UIViewController gl_view: ?*UIView (SxGLView OR SxMetalView — both extend UIView) gl_layer: ?*CALayer (CAEAGLLayer OR CAMetalLayer) gl_ctx: ?*EAGLContext display_link: ?*CADisplayLink Each field is wrapped in '?' since the platform may not have set it yet (gl_ctx is null in metal mode, display_link is null before the first frame, etc.). SxSceneDelegate's window getter/setter now take/return '?*UIWindow' instead of '*void' so calling code doesn't need an xx-cast. Required fix in objcTypeEncodingFromSignature: '?T' (optional) was bailing with 'type kind not yet supported'. Apple's runtime treats nullability as 'pointer may be null' — the wire encoding is the same as T. Recursive unwrap handles ?*UIView → '@', ?*CADisplayLink → '@', etc. Chess on iOS-sim: board renders, full pipeline intact. 183 tests + zig build test green. |
||
|
|
d403f56673 |
uikit: inline three trivial legacy IMP helpers into #objc_class methods
Three holdover free functions from the pre-M3 era were each
just two or three lines that forwarded to a global. With M3
finished, every call site is one #objc_class method body, so
the wrapper indirection earns nothing — inline them.
Deleted:
uikit_window_getter → body of SxSceneDelegate.window
uikit_window_setter → body of SxSceneDelegate.setWindow
uikit_did_finish_launching → body of
SxAppDelegate.application_didFinishLaunchingWithOptions
The bigger helpers (uikit_keyboard_will_change_frame,
uikit_scene_will_connect, uikit_gl_view_tick/_layout, the four
uikit_gl_view_touches_*) stay — their bodies are 30-80 lines
each, so wrapping them in a small forwarding method body inside
#objc_class is the cleaner factoring.
Chess on iOS-sim: board renders, full game state intact. 183
example tests + zig build test green.
|
||
|
|
78288b98ac |
ffi M3.3 + M3.4 + M3.5: SxGLView/SxMetalView migrated; uikit_register_classes deleted
Three slices in one commit since they're tightly coupled (the
M3.5 deletion only makes sense after M3.3 and M3.4):
M3.3 — SxGLView migrated to declarative '#objc_class("SxGLView")':
- '#extends UIView' for the view-hierarchy + responder chain.
- 'layerClass :: *void = objc_getClass("CAEAGLLayer".ptr);' uses
the M2.1(a) class-level constant form. Registered on the
metaclass; UIView's +layerClass override dispatches here so
EAGL gets the right backing layer.
- Six instance methods (sxTick, layoutSubviews, four touch
selectors) forward to existing legacy IMP free functions.
M3.4 — SxMetalView migrated, same shape as SxGLView; differs only
in the 'layerClass' constant returning CAMetalLayer instead of
CAEAGLLayer. The five shared IMPs (sxTick/layoutSubviews/4 touch
handlers) reach the same free functions — they already branch on
plat.gpu_mode for GL-specific renderbuffer code.
M3.5 — uikit_register_classes() and the two helper registration
functions are deleted outright. Every sx-defined Obj-C class in
this module now goes through the compiler's M1.2 / M2.1(a)
synthesis path at module init. The call site inside
UIKitPlatform.init is gone too — just a comment marking the
migration point.
Chess on iOS-sim: board renders, scene-delegate connection still
fires, GL/Metal layer setup intact, touch dispatch routes through
the synthesized IMP trampolines. 183 example tests + zig build
test green.
End of M3. The platform layer's Obj-C-runtime wiring is fully
declarative.
Remaining: M4 (autoreleasepool + ARC ops), M5 (closure↔block),
M6 (auto-import + production hardening). M1.1.b (Class(T)
parameterization + instancetype) is still deferred — none of
the migrated uikit code needed it.
|
||
|
|
066840d9e0 |
ffi M3.2: SxSceneDelegate migrated + #implements protocol conformance
Migrates SxSceneDelegate from the hand-rolled
objc_allocateClassPair + class_addMethod + class_addProtocol
sequence to the declarative form:
SxSceneDelegate :: #objc_class("SxSceneDelegate") {
#extends UIResponder;
#implements UISceneDelegate;
#implements UIWindowSceneDelegate;
scene_willConnectToSession_options :: (self, scene, session, options) { ... }
window :: (self) -> *void { ... }
setWindow :: (self, w) { ... }
}
emit_llvm now honors '#implements' in the class-pair init
constructor — for each #implements ProtocolAlias on the cache
entry's AST, emit before objc_registerClassPair:
proto = objc_getProtocol("ProtocolName")
class_addProtocol(cls, proto)
iOS checks 'class_conformsToProtocol' when instantiating scene
delegates; without the conformance the runtime silently rejects
the class and a default scene with no delegate gets created
instead. The protocol-getter returns null on dead-strip /
runtime mismatch (rare but possible) — the runtime treats
class_addProtocol(cls, null) as a no-op, so no explicit null
check needed.
Method bodies forward to the existing legacy free IMP functions
(uikit_scene_will_connect, uikit_window_getter,
uikit_window_setter) so we don't have to inline the scene-
connect setup logic (~80 lines).
uikit_register_classes is now tiny — just the two remaining
view-class helpers (M3.3 SxGLView + M3.4 SxMetalView). M3.5
deletes the function entirely once those land.
Chess on iOS-sim: board renders, scene delegate fires, touch
events route correctly. 183 example tests + zig build test
green.
|
||
|
|
66f84f67b8 |
ffi M3.1 + M1.2 A.3 refactor: self=Obj-C id, self.field via ivar; SxAppDelegate migrated
Two coupled changes that unblock the uikit_register_classes
migration:
1) M1.2 A.3 — body's 'self' is the Obj-C id (opaque), NOT the
state struct. Matches Apple's ObjC semantics where 'self' IS
the object. Cocoa idiom 'xx self → id' works at runtime calls
(addObserver:, etc.); previously the trampoline replaced
'self' with the state-struct pointer, breaking any runtime
call that expected an id.
'*Self' substitution in resolveTypeWithBindings now points at
foreignClassStructType(fcd) — the opaque class stub — instead
of objcDefinedStateStructType(fcd).
'self.field' access on a sx-defined class instance field is
rewritten by lowerFieldAccess to go through the __sx_state
ivar:
state = object_getIvar(self, load(__<Cls>_state_ivar))
val = struct_gep(state, field_idx) → load
Both read (lowerFieldAccess) and write (lowerAssignment) take
this path. Compound ops (+=, -=, etc.) are supported via
storeOrCompound. The lookup is filtered: skip property fields
(those still go through the M2.2 msgSend getter/setter
dispatch) and foreign classes (no state).
New helpers in lower.zig:
- lookupObjcDefinedStateFieldOnPointer — match check.
- lowerObjcDefinedStateForObj — emit the object_getIvar +
ivar-global-load idiom (shared between read + write paths).
- lowerObjcDefinedStateFieldRead — the load path.
Also moved the @llvm.global_ctors registration out of the
sx-defined class-pair init constructor — global_ctors fires
DURING dyld's framework load, before UIKit registers its Obj-C
classes. objc_getClass("UIResponder") returned null, super
was null, objc_registerClassPair crashed. main's entry block
is post-framework-load but pre-user-code — exactly the right
window. New helper injectCtorIntoMain.
2) M3.1 — SxAppDelegate migrated to declarative #objc_class.
uikit_register_classes' hand-rolled objc_allocateClassPair +
class_addMethod for SxAppDelegate is gone; the compiler
synthesises the class at module init. The method bodies
forward to the existing legacy IMP free functions
(uikit_did_finish_launching, uikit_keyboard_will_change_frame)
so we don't have to inline 70+ lines of keyboard-frame logic
right now.
Also adds UIResponder foreign-class declaration and chains
UIView / UITextField to it via #extends UIResponder so the
methods that previously lived on UITextField directly
(becomeFirstResponder etc.) move to their proper home.
Chess on iOS-sim: board renders, full state intact. 183 example
tests + zig build test green.
|
||
|
|
a1736f3213 |
ffi M1.2 A.5: synthesized +alloc IMP + ensureCRuntimeDecl helper
For every sx-defined #objc_class, emit a C-callconv +alloc IMP
that the Obj-C runtime calls when '[Cls alloc]' fires (from sx
code, UIKit instantiation, Info.plist principal class, etc.):
+alloc IMP (cls: Class, _cmd: SEL) -> id
instance = class_createInstance(cls, 0)
state = malloc(STATE_SIZE)
memset(state, 0, STATE_SIZE)
object_setIvar(instance, load(@__<Cls>_state_ivar), state)
return instance
STATE_SIZE = max(typeSizeBytes(state struct), 1) — always at
least one byte so the ivar is never null after +alloc returns.
The IMP is registered on the METACLASS (class methods live there
— every Class object's isa points to the metaclass) in emit_llvm's
class-pair init constructor:
metaclass = object_getClass(cls)
sel_alloc = sel_registerName("alloc")
class_addMethod(metaclass, sel_alloc, alloc_imp, "@@:")
That override wins over NSObject's default +alloc; runtime
instantiations get the __sx_state ivar bound automatically.
Per-instance allocator binding (the plan's full design — store
the Allocator value in the state struct so -dealloc frees through
the same one) is deferred. libc malloc/free is fine for v1; we'll
upgrade once Month 4's autoreleasepool + ARC ops shake out.
REFACTOR: collapsed five duplicate 'get<Name>Fid' helpers and
their cache fields (object_getIvar, object_setIvar,
class_createInstance, malloc, memset) into a single
'ensureCRuntimeDecl(name, params, ret) -> FuncId'. The helper
checks for an existing decl by name first (avoids the
'class_createInstance.1' duplicate-symbol crash when stdlib's
'#foreign' decl is already in the module). One helper instead
of one-per-function = ~150 lines deleted.
object_getIvar / object_setIvar added to stdlib std/objc.sx
so user code can use them too (146 exercises object_getIvar
to verify __sx_state was bound to a non-null state pointer
after +alloc).
146-objc-class-alloc-roundtrip.sx end-to-end against macOS:
'[SxFoo alloc]' returns non-null AND object_getIvar(instance,
__sx_state) returns the state ptr. Real Obj-C runtime, no
mocks.
175 example tests pass (+1). zig build test green.
|
||
|
|
d9dbdad3f5 |
ffi M1.1 (first pass): id / Class / SEL / BOOL type aliases
Adds named stand-ins for the three opaque Obj-C runtime types and Apple's signed-char boolean to library/modules/std/objc.sx: id :: *void; // any Obj-C instance pointer Class :: *void; // a class object pointer SEL :: *void; // a registered selector BOOL :: s8; // Apple's signed-char boolean (NOT sx's bool) All resolve to their underlying type at the LLVM layer — no runtime cost — but make foreign-class declarations read closer to Objective-C source. The header's old caveat about lacking type aliases is gone. 141-objc-type-aliases.sx exercises the aliases against the real macOS Obj-C runtime: alloc/init an NSObject, fetch its class via objc_getClass, sel_registerName a SEL, then call 'isKindOfClass:' returning BOOL=1. Non-macOS paths print the same line to keep the snapshot stable. DEFERRED (M1.1.b, follow-up): 'Class(T)' parameterization with #extends-aware covariance, and 'instancetype' per-decl substitution. Both require compiler-level type-check support beyond plain stdlib aliases. 170 examples pass (+1). |
||
|
|
bd3033dc5a |
ffi 3.2 C5: migrate uikit.sx view tree + GL drawables cluster
Final Phase 3.2 cluster. CALayer / CAEAGLLayer / EAGLContext declared
as `#foreign #objc_class` blocks, plus `setContentScaleFactor` added
to UIView and `-layer` now returns `*CALayer` (was opaque `*void`).
Classes declared:
- CALayer → setOpaque (instance)
- CAEAGLLayer → setDrawableProperties (instance)
- EAGLContext → alloc (class), initWithAPI (instance),
setCurrentContext (class — takes EAGLContext arg),
renderbufferStorage_fromDrawable (instance),
presentRenderbuffer (instance)
- UIView → +setContentScaleFactor (existing decl extended)
The C5 group sits above UIView in the file so the `-layer` return type
`*CALayer` forward-resolves cleanly.
Migration sites in uikit.sx:
- `uikit_create_gl_context` → `EAGLContext.alloc().initWithAPI(api)`
+ `EAGLContext.setCurrentContext(ctx)`.
- `uikit_setup_renderbuffer` → cast `*EAGLContext` and
`gl_ctx.renderbufferStorage_fromDrawable(target, layer)`.
- `uikit_present_renderbuffer` → same cast + `presentRenderbuffer(target)`.
- Scene-connect bring-up: `gl_layer.setOpaque(1)`,
`eagl_layer.setDrawableProperties(...)`,
`gl_view.setContentScaleFactor(scale)`.
One more `objc_getClass(...)` lookup (EAGLContext) retired — the class
slot comes from the declarative binding via `__sx_objc_class_init`.
**Phase 3.2 complete.** Five clusters migrated (C1: Foundation
utility; C2: Notifications + Bundle; C3: RunLoop + display timing;
C4: UIKit chrome; C5: view tree + GL drawables). 8 foreign Cocoa
classes declared; ~30 `#objc_call(T)(...)` sites rewritten to
`recv.method(args)` / `Cls.method(args)`; 6 `objc_getClass`
lookups retired. Sx-defined classes (SxAppDelegate, SxSceneDelegate,
SxGLView, SxMetalView) and a handful of foreign sites that exercise
delegate/protocol surfaces stay on the explicit `#objc_call` form
pending Phase 3.7's class synthesis.
167/167 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
|
||
|
|
5b4969f9be |
ffi 3.2 C4: migrate uikit.sx UIKit chrome cluster to #objc_class
Fourth cluster — was blocked on issue-0043, now unblocked by the
preceding `Self`-substitution fix.
Classes declared:
- UIScreen → mainScreen (class), nativeScale + bounds (instance)
- UIView → safeAreaInsets, addSubview, layer (all instance)
- UIWindow → alloc (class), initWithWindowScene, setRootViewController,
makeKeyAndVisible, screen (instance)
- UIViewController → alloc (class), init, setView (instance)
- UITextField → alloc (class), init, becomeFirstResponder,
resignFirstResponder (instance)
Migration sites in uikit.sx:
- `show_keyboard` / `hide_keyboard` → `tf.becomeFirstResponder()` /
`tf.resignFirstResponder()` on a `*UITextField` cast of `text_field`.
- `uikit_refresh_safe_insets` → `gl_view.safeAreaInsets()` on a
`*UIView` cast of `plat.gl_view`.
- `uikit_read_screen_scale` and GL-context bring-up →
`UIScreen.mainScreen().nativeScale()`.
- `uikit_keyboard_will_change_frame` → `win.screen().bounds()`.
- `uikit_scene_will_connect_ios` (the function that triggered 0043) →
`UIWindow.alloc().initWithWindowScene(scene)`,
`UIViewController.alloc().init()`, `vc.setView(...)`,
`win.setRootViewController(...)`, `gl_view.layer()`,
`UITextField.alloc().init()`, `gl_view.addSubview(...)`,
`win.makeKeyAndVisible()`.
Three `objc_getClass(...)` lookups (UIWindow, UIViewController,
UITextField) are gone — the class slots come from the declarative
bindings via `__sx_objc_class_init`. UIScreen has the same shape.
167/167 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
|
||
|
|
2a7c8e0a6f |
ffi 3.2 C3: migrate uikit.sx RunLoop + display-timing cluster
Third cluster: NSRunLoop and CADisplayLink move to declarative
`#objc_class` blocks.
Classes declared:
- NSRunLoop → currentRunLoop (class)
- CADisplayLink → displayLinkWithTarget_selector (class),
addToRunLoop_forMode (instance),
targetTimestamp (instance), duration (instance)
The display-link instance is created with the new typed call shape:
link := CADisplayLink.displayLinkWithTarget_selector(plat.gl_view, sel_tick);
plat.display_link = xx link; // keep the *void slot in the
// platform struct for ABI parity
runloop := NSRunLoop.currentRunLoop();
link.addToRunLoop_forMode(runloop, mode_ns);
The `sxTick:` callback's `link: *void` param is cast to
`*CADisplayLink` at function entry so the body's `link.duration()` /
`link.targetTimestamp()` calls type-check.
Two now-redundant `objc_getClass(...)` lookups for CADisplayLink /
NSRunLoop are gone — the class slots come from the declarative
declarations via `__sx_objc_class_init`.
166/166 tests; chess builds clean on macOS / iOS / Android.
|
||
|
|
17775b27a4 |
ffi 3.2 C2: migrate uikit.sx Notifications + Bundle cluster
Second cluster: NSNotification, NSBundle, NSNotificationCenter move
from `#objc_call(T)(recv, "sel:", args)` to declarative
`#foreign #objc_class("Cls") { ... }` blocks.
Classes declared (alongside the C1 Foundation utility group):
- NSNotification → userInfo (instance, returns *NSDictionary)
- NSBundle → mainBundle (class), resourcePath (instance)
- NSNotificationCenter → defaultCenter (class),
addObserver_selector_name_object (instance)
The 4-keyword `addObserver:selector:name:object:` selector derives
from the underscore-separated sx name via the default mangling rule
— no `#selector` override needed.
Cleanup wins:
- `objc_getClass("NSBundle")` and `objc_getClass("NSNotificationCenter")`
call sites gone — class slots now populated by emit_llvm's
`__sx_objc_class_init` constructor.
- `userInfo`'s return type is `*NSDictionary` directly, so the
previous `*void → *NSDictionary` cast at the keyboard-frame
callsite collapses.
166/166 tests; chess builds clean on macOS + iOS + Android via
`sx build main.sx`.
|
||
|
|
1ea9cda12b |
ffi 3.2 C1: migrate uikit.sx Foundation utility cluster to #objc_class
First of five Phase-3.2 migration clusters. Foundation utility
classes (NSValue, NSNumber, NSDictionary, NSMutableDictionary, NSSet)
in `library/modules/platform/uikit.sx` move from the explicit
`#objc_call(T)(recv, "selector:", args)` form to declarative
`#foreign #objc_class("Cls") { ... }` blocks with `recv.method(args)`
dispatch.
Classes declared (all near the top of uikit.sx, after the CGRect
struct):
- NSValue → CGRectValue (instance)
- NSNumber → numberWithBool (class), doubleValue +
unsignedLongValue (instance)
- NSDictionary → objectForKey (instance)
- NSMutableDictionary → dictionary (class), setObject_forKey (instance)
- NSSet → anyObject (instance)
Each call site casts the `*void` receiver to the typed foreign-class
pointer before dispatch — the existing `*void` shape is preserved
in the struct fields and outer parameter types; only the dispatch-
local copy is typed. This keeps the diff scoped to call-site
rewrites without rippling type changes through every consumer.
The four `objc_getClass(...)` calls that previously resolved
NSMutableDictionary / NSNumber at runtime are gone — Phase 3.1's
`__sx_objc_class_init` constructor populates the cached class slot
for each declared `#objc_class` at module load via
`OBJC_CLASSLIST_REFERENCES_<Cls>`.
166/166 example tests; chess clean on macOS + Android via
`tools/verify-step.sh` (iOS sim skipped — no booted simulator in
this run; previous full run was green at HEAD~6).
|
||
|
|
56414407fc |
ffi: drop static keyword on foreign-class methods; param type discriminates
`static name :: ...` was redundant — instance methods always declare `self: *Self` as their first param by convention. The parser now derives `is_static` from the first param's TYPE: if it's `*Self` the method is an instance method; anything else (including no params at all) is a class method. Removes a token from the surface, keeps the dispatch behavior identical. The receiver param's NAME doesn't matter — only its type. Calling the first param `this`, `me`, `receiver`, etc. is fine as long as the type is `*Self`. This mirrors how the rest of sx handles receiver dispatch. Migration of every site that used the keyword: - `library/modules/platform/android.sx` — `SurfaceView.new(ctx)`. - `examples/ffi-jni-class-03-static.sx` — `Math.abs(n)`. - `examples/ffi-jni-main-03-ctor.sx` — `SurfaceView.new(ctx)` in the `#jni_main` body. - `examples/ffi-objc-dsl-05-static.sx` — NSObject's `.class()` / `.description()`. 164/164 example tests; chess clean on macOS / iOS sim / Android via `tools/verify-step.sh`. |
||
|
|
da1063f1bb |
mem: allocator init returns state by value (drops state-struct heap alloc)
Building on the Option 3 lvalue-borrow rule, the long-lived allocators
in `library/modules/allocators.sx` (GPA, Arena, TrackingAllocator) now
return their state by value instead of via a heap-allocated `*T`. The
caller binds the result to a local; the local IS the allocator state.
`xx local` borrows that storage under Option 3, so the `Allocator`
protocol value's `ctx` points at the local — no heap allocation for
the state struct, no `free` of the state needed.
```sx
gpa := GPA.init(); // GPA (value)
arena := Arena.init(xx gpa, 4096); // Arena (value)
tracker := TrackingAllocator.init(xx gpa); // TrackingAllocator (value)
push Context.{ allocator = xx tracker, data = null } { ... }
```
Why by-value:
- One fewer `libc_malloc` per allocator instance.
- No state-struct leak. The local is reclaimed at scope exit; `deinit`
only handles downstream resources (chunks, etc.) — not its own struct.
- Owning structs can embed allocators as value fields directly.
Callsite changes:
- `library/modules/ui/pipeline.sx`: `arena_a: Arena;` / `arena_b:
Arena;` (was `*Arena;`). The `build_arena: *Arena` local takes
`@self.arena_a` / `@self.arena_b`.
- `examples/126-xx-recover-then-dispatch.sx`: `recovered == @gpa`
instead of `recovered == gpa` (gpa is a value now).
- `examples/135-xx-lvalue-borrows.sx`: drop the `tracker_ptr.*`
deref — `init` already returns the value.
- `examples/50-smoke.sx`: Arena alloc counts dropped by 1 (no
state-struct allocation). Comments + snapshot updated.
`Arena.deinit` drops the trailing `parent.dealloc(xx a)` — the
caller's local owns the storage.
FFI IR snapshots regenerated to reflect the new signatures:
`@GPA.init` returns `i64` (was `ptr`); `@Arena.init` and
`@TrackingAllocator.init` use sret returns (was `ptr`).
CLAUDE.md "Allocator construction" rule rewritten around the
by-value convention. The forbidden caller-provides-storage and
redundant-pointer-rename patterns are still forbidden but for the
right reasons now (verbose, fragile) rather than as a workaround
for the old `init() -> *T` shape.
157/157 example tests pass; chess clean on macOS, iOS sim, and
Android via `tools/verify-step.sh`.
|
||
|
|
72593db953 |
mem: List(T) mutations gain optional alloc: Allocator = context.allocator
The chess panel-text regression (text vanished after the first move on macOS) had a single root cause: GlyphCache's entries List, hash table, and shaped_buf grew through `context.allocator` — which during render is the per-frame arena. On the next arena reset the backing died, and subsequent glyph lookups read garbage / wrote into freshly-allocated view-tree memory. Fix is shaped as the user proposed: `List(T)`'s mutations take an optional trailing `alloc: Allocator = context.allocator` argument. No allocator stored on the container, no init ceremony, every existing `list.append(item)` callsite keeps working unchanged. Long-lived owners now write `list.append(item, self.parent_allocator)` and the arena-leak bug becomes impossible to write accidentally. Default-arg substitution previously only fired for identifier callees (`expandCallDefaults` at lower.zig:7978). Extended to the generic struct-method dispatch path (`list.append(...)` lands here) via a new `appendDefaultArgs` helper that lowers fd.params[i].default_expr in the caller's scope and appends to the lowered args slice. Long-lived owners updated to capture `parent_allocator: Allocator` at init and use it for every internal growth: - GlyphCache (the chess bug) — entries, shaped_buf, hash_keys, hash_vals, atlas bitmap. - DockInteraction — drops the existing `push Context` workaround in `ensure_capacity` for the explicit-arg form. - StateStore — entries list + per-entry data buffer. - Gles3Gpu, MetalGPU — shaders, buffers, textures (atlas-grow during render would otherwise leak resources into the frame arena). Also kept: an operator-precedence fix in pipeline.sx (`(self.frame_index & 1) == 0` instead of `self.frame_index & 1 == 0`, which parses as `self.frame_index & (1 == 0)` = always 0). That was a stealth single-arena-only bug that masked the GlyphCache one for a long time. Docs: - specs.md §11 documents `param: T = expr` default parameter values. The parser already supported it — formalised in the spec now. - current/CHECKPOINT-MEM.md logs the change. - CLAUDE.md REJECTED PATTERNS gains a "Long-lived containers growing through context.allocator" section with the `parent_allocator` capture template and the list of existing examples to mirror. 155/155 example tests pass — zero-diff against snapshots since every existing callsite still resolves to `context.allocator`. |
||
|
|
f886d5f1be |
mem: reject call-conv mismatches at bare-fn → fn-ptr coercion
Passing a default-conv sx function to a `callconv(.c)` fn-pointer slot (e.g. pthread_create's start routine) used to silently mismatch ABIs: the C-side caller didn't supply __sx_ctx, so the sx-side body read its first user param as garbage. The bug surfaced as a SIGSEGV inside ANativeWindow_setBuffersGeometry on Android during chess bringup. Now the compiler rejects the coercion outright at the bare-fn name lookup site: error: call-convention mismatch: 'sx_handler' is declared with default sx convention but the target type expects callconv(.c) Also: `#foreign` declarations without an explicit `callconv` now default to `.c` instead of `.default`. Every external C symbol is by definition C-conv; the previous default silently typed `objc_msgSend` (et al.) as default-conv, so the check would fire on the consumer side when the user typed a fn-ptr as `callconv(.c)`. With the foreign-default fix, the existing typed-msgSend casts in `std/objc.sx` and `gpu/metal.sx` keep type-checking and the rule is "C-conv on both sides or neither." Caught by the new check (fixed in the same commit): - `ios_gl_proc` in `platform/uikit.sx` lacked callconv(.c) but was passed to `load_gl` whose `get_proc` slot expects it. - `ffi_apply_callback` / `ffi_apply_callback2` in `examples/ffi-06-callback.sx` had default-conv fn-ptr params but the C bodies (in the companion .c) are unambiguously C-conv. Regression test: `examples/131-callconv-mismatch-diagnostic.sx` locks in the diagnostic shape (sx-conv fn → callconv(.c) slot). 153/153 example tests pass. Chess green on macOS / iOS sim / Android. |
||
|
|
d4a342d0c1 |
mem: implicit-Context platform fixes — chess green on macOS/iOS/Android
Verify-step uncovered three categories of regressions where sx code
calls into the platform's C ABI through fn-pointer types or as a
registered callback. Every site now declares the right convention.
C-side calls INTO sx → callconv(.c) on the sx function:
- platform/android.sx: sx_android_render_thread_entry is the start
routine pthread_create invokes — pthread treats it as a C function.
Also annotate the pthread_create signature so the start-routine fn-
pointer field rejects mismatching sx fns at compile time.
sx code calling typed fn-pointers cast from C symbols → callconv(.c)
on the fn-pointer type:
- opengl.sx: 55 GL fn-ptr globals + load_gl's proc-loader param. GL
trampolines are macOS/iOS/Android system code.
- std/objc.sx: the two typed `objc_msgSend` casts.
- gpu/metal.sx: ~40 typed `objc_msgSend` casts across Metal command
encoder / device / pipeline construction.
The block invoke trampolines (objc_block.sx) call back INTO sx (the
closure trampoline). The typed fn-ptr there stays default-conv so
ctx prepends correctly. Compiler change: a callconv(.c) sx function
now binds `current_ctx_ref` to `&__sx_default_context` at entry (used
to be gated by `isExportedEntryName`). C-callable sx callbacks like
the block invokes don't get their own __sx_ctx param but their bodies
still need a real Context to forward to the closure they delegate to.
Tests: 152/152 example suite + chess green on all 3 platforms.
Screenshots at /tmp/sx-game-{macos,iossim,android}.png.
|
||
|
|
b69a2ea29c |
mem: Step 8 — delete context global from std.sx
The `context : Context = ---;` global in `library/modules/std.sx` had
no remaining readers — all `context.X` lookups in user code resolve
through `current_ctx_ref` (Step 5), `push Context.{...}` uses an alloca
slot (Step 6), and `allocViaContext` sources from the lowering's
current ref. `emitDefaultContextInit` (the only writer) was already
removed in Step 5.
`inferExprType` for the `context` identifier now returns the registered
`Context` type when implicit-ctx is enabled, mirroring the lowering's
identifier-handling fast path. Without this, `context.allocator` would
type as `s64` (the fallback) and the field access would fail.
11 JNI/ObjC IR snapshots regen — the `@context` LLVM global is gone
from each.
152/152 example tests pass.
|
||
|
|
29784c22a8 |
mem: implicit-context foundation + many compiler fixes
The session-long set of changes that lay the groundwork for the
Jai-literal implicit-Context-parameter refactor. Lots of accumulated
work; the new arrival is the implicit-ctx foundation (steps 1+2 of
the plan in current/CHECKPOINT-MEM.md):
Step 1 — `CAllocator :: struct {}` stateless allocator in
library/modules/allocators.sx, delegating directly to
libc_malloc/libc_free. `ConstantValue` in src/ir/inst.zig gains a
`func_ref: FuncId` leaf so nested aggregates can carry function
pointers (the inline Allocator value's fn-ptr fields). Switch
sites updated in emit_llvm.zig, print.zig, interp.zig.
Step 2 — `emitDefaultContextGlobal` in src/ir/lower.zig synthesises
a static `__sx_default_context` global with a nested-aggregate
init_val pointing at the CAllocator → Allocator thunks. The
second-pass `initVtableGlobals` in emit_llvm.zig is generalised
to handle `.aggregate` init_vals (re-emits after func_map is
populated so func_ref leaves resolve to real symbols).
Also folded in from earlier work this session:
- Phase 1.1: `xx value` heap-copy in `buildProtocolValue` routes
through `context.allocator` via the new `allocViaContext` helper.
- interp.zig: `marshalForeignArg` double-offset bug fixed —
`heapSlice` already adds `hp.offset` to the slice ptr, so the
extra `+ hp.offset` was scribbling memcpy/memset into adjacent
heap state, corrupting `heap.items[0]`. Symptom: `build_format`
at comptime produced zero bytes, all `print` calls failed.
- Lazy lowering: `lazyLowerFunction` now declares foreign-body
functions as extern stubs in the local (comptime) module so
cross-module foreign calls resolve.
- Allocator API: all stdlib allocators on one-line `init() -> *T`
(CAllocator/GPA: libc-backed; Arena/TrackingAllocator: parent-
backed; BufAlloc: embeds state at head of user buffer).
- issues 0038 (transitive #import), 0039 (chess + stdlib migration
fallout), 0040 (generic struct method dot-dispatch), 0041
(pointer types as type-arg), 0042 (alias name resolution) — all
fixed; regression tests in examples/.
- Diagnostic: `emitError` now embeds the lowering's
`current_source_file` and enclosing function in the literal
message; SX_TRACE_UNRESOLVED=1 dumps a Zig stack trace at the
emit site so misattributed spans can't hide where the failure
is.
- tools/verify-step.sh (all-platforms gate) and tools/scratch.sh
(interp/codegen parity tester) added.
Test suite: 152 example tests pass; chess builds + screenshots on
macOS / iOS sim / Android.
|
||
|
|
49b39ba07a | ... | ||
|
|
632e64512b |
bundling: Android APK pipeline moved into sx; android.sx state-on-plat
Week 7 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
plus the android.sx refactor + three sx-compiler fixes hit along the way
to get chess on Pixel 7 Pro responding to touch end-to-end.
library/modules/platform/bundle.sx now covers the Android APK shape
alongside macOS / iOS-sim / iOS-device. `android_bundle_main` discovers
the SDK ($ANDROID_HOME / $ANDROID_SDK_ROOT / $HOME/Library/Android/sdk),
picks the highest-versioned build-tools + platforms via
`process.run("ls .. | sort -V | tail -1")`, stages
`<apk>.stage/lib/arm64-v8a/<libfoo.so>`, synthesizes
AndroidManifest.xml (NativeActivity vs `#jni_main` Activity branch),
writes each `#jni_main` decl's Java source under
`<stage>/java/<pkg>/<Cls>.java`, runs javac --release 11 + d8 to
produce classes.dex, aapt2-links the unaligned APK, appends lib/ +
classes.dex + each registered asset tree via zip, zipalign + ensure
debug keystore via keytool + apksigner sign.
Compiler-side accessors (src/ir/compiler_hooks.zig + library/modules/compiler.sx):
- is_android predicate.
- set_manifest_path / manifest_path + set_keystore_path / keystore_path.
- jni_main_count / jni_main_foreign_path_at(i) /
jni_main_java_source_at(i) surface the `#jni_main` emissions that
the Zig createApk previously consumed directly.
- main.zig wires manifest_path, keystore_path, and the per-decl
(foreign_path, java_source) parallel slices into BuildConfig before
invoking the post-link callback.
CLI `--apk <path>` keeps working as a transitional alias: it now feeds
bundle_path so the existing auto-`post_link_module = "platform.bundle"`
shim fires the same way as `--bundle`. main.zig no longer calls
target.createApk directly.
Deletions in src/target.zig: createApk, compileJniMainSources,
buildJniMainManifest, buildAndroidManifest, ensureDebugKeystore,
libNameFromSoBasename, plus helpers splitForeignPath / discoverJavac /
discoverAndroidSdk / findHighestSubdir / runProcess / runProcessIn
(~400 lines). git grep returns only the obituary comment.
library/modules/platform/android.sx refactor (chess Android dependency):
- Module-level globals retired (g_app_window, g_egl_*, g_viewport_*,
g_dpi_scale, g_should_stop, g_render_thread*, g_user_main_fn,
g_touch_*) → AndroidPlatform struct fields.
- All sx_android_* helpers take `plat: *AndroidPlatform` as first arg.
Render thread receives plat via pthread_create's arg.
- New `logical_w: f32 = 0.0` field. Consumers set it before init() to
define the design width in points; `recompute_scale` derives
`dpi_scale = pixel_w / logical_w` (or 1.0 if unset). Called on
init / set_viewport / egl_init. drain_touches divides incoming
physical pixel coords by dpi_scale so chess sees logical-space
positions matching its layout. Touch lands on the right squares.
Three sx-compiler bugs hit + fixed along the way:
1. Top-level `inline if OS == .X { decls }` body decls were silently
dropped because scanDecls/lowerDecls had no .if_expr arm. New
`flattenComptimeConditionals` pre-pass in src/imports.zig
(threaded via ComptimeContext from core.zig) hoists matching arms
recursively. Regression at examples/124-inline-if-hoist-toplevel.sx.
2. Parser rejected `#import` / `#framework` inside inline-if bodies
because parseStmt in src/parser.zig only had arms for `#insert`.
Added the missing arms. Regression at
examples/123-inline-if-import-in-body.sx (landed earlier).
3. JNI `Call<T>Method` switches in src/ir/emit_llvm.zig (instance /
nonvirtual / static) were missing `.f32` rows — jfloat returns
(e.g. MotionEvent.getX/getY) fell into the silent-undef else arm.
Chess's sx_android_push_touch(plat, getAction(), getX(), getY())
delivered garbage f32 coords to the touch ring, so taps landed
nowhere recognisable. Added `.f32 => Jni.Call{Static,Nonvirtual,}FloatMethod`
rows to all three switches; lifted unsupported-type detection
from emit_llvm into lowerForeignMethodCall with proper
source-spanned diagnostics (`isJniReturnTypeSupported`). Regressions
at examples/ffi-jni-call-10-jfloat-return.sx,
examples/ffi-jni-class-09-multi-float-args.sx,
examples/ffi-jni-call-11-unsupported-return-diag.sx.
Stale-snapshot drift in tests/expected/ffi-objc-call-03-selector-sharing.ir
and ffi-objc-call-06-sret-return.ir picks up the new BuildOptions
accessor extern decls (is_android, set_manifest_path,
set_keystore_path, jni_main_count, jni_main_foreign_path_at,
jni_main_java_source_at). Verified diff is dead-decl-only.
Chess on Pixel 7 Pro: tap on e2 white pawn -> yellow selection +
green dots on legal e3/e4 targets; tap on e4 -> board updates with
1. e4, "Black to move" + "1. e4" in info panel.
zig build && zig build test && bash tests/run_examples.sh -> 145/145
green. bash tests/cross_compile.sh -> 7/7 green.
|
||
|
|
5cc62e63c3 |
bundling: fs/process stdlib + post-link callback + Apple .app in sx
Campaign Weeks 3-6 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
land in one push: the bundling pipeline that used to live in
src/target.zig (createBundle, embedFramework, extractEntitlements,
buildInfoPlist, codesign) now lives in
library/modules/platform/bundle.sx and runs in the IR interpreter
after target.link() returns.
New language-side surface:
- library/modules/fs.sx — POSIX libc bindings (open/read/write/close,
mkdir/unlink/rmdir, chmod, rename, access, basename/dirname). Variadic
open() lowers to C's varargs via the new args: ..T form. Direct libc
calls bypass *File method dispatch so they work from the post-link
IR interpreter.
- library/modules/process.sx — popen-based run(cmd) returning
ProcessResult{ exit_code, stdout }, plus env() and find_executable().
- library/modules/std.sx — xml_escape(s) and variadic path_join(parts).
- library/modules/compiler.sx — BuildOptions grows
set_post_link_callback / set_post_link_module / binary_path
accessors; bundle_path/bundle_id/codesign_identity/provisioning_profile
setters + accessors; per-target predicates is_macos/is_ios/
is_ios_device/is_ios_simulator + target_triple; framework_count /
framework_at(i) / framework_path_count / framework_path_at(i);
add_asset_dir(src, dest) + asset_dir_count / src_at / dest_at.
Compiler-side wiring:
- src/ir/compiler_hooks.zig — BuildConfig now carries post_link_callback_fn,
post_link_module, binary_path, bundle_*, target_triple,
target_frameworks, target_framework_paths, asset_dirs. Hook registry
exposes every accessor; getters return "" / 0 for unset fields so
bundle.sx can treat absent values uniformly.
- src/ir/host_ffi.zig (new) — dlsym(RTLD_DEFAULT) + arity-switched cdecl
trampolines so #foreign("c") declarations resolve through the host
libc during #run / post-link interpretation.
- src/ir/interp.zig — callForeign dispatch; build_config pointer
injection so accessor hooks see live state during re-entry.
- src/core.zig — keeps the IR module alive past generateCode; exposes
invokeByName / invokeByFuncId so main.zig can re-enter the
interpreter after linking.
- src/main.zig — wires bundle/codesign/provisioning CLI flags +
target_triple + framework lists into BuildConfig; invokes the
post-link callback (by FuncId or by <module>.bundle_main lookup) once
target.link() returns. When --bundle is set but no callback is
registered, auto-falls-back to post_link_module = "platform.bundle"
so the legacy --bundle CLI keeps working for any program that imports
modules/platform/bundle.sx.
Apple .app bundler (library/modules/platform/bundle.sx):
- Single bundle_main entry covers macOS, iOS simulator, iOS device.
Per-target Info.plist switch keys off is_ios()/is_ios_simulator() —
iOS emits UIDeviceFamily / LSRequiresIPhoneOS /
UIApplicationSceneManifest / DTPlatformName (iPhoneOS or
iPhoneSimulator); macOS emits the minimal CFBundle* set.
- iOS-only steps:
- Provisioning embed: fs.read_file + fs.write_file to
<bundle>/embedded.mobileprovision.
- Framework embed: recursive cp -R per -F search path into
<bundle>/Frameworks/<Name>.framework/ (until fs.sx grows list_dir).
- Entitlements extraction: four process.run calls (security cms -D,
plutil -extract Entitlements xml1, plutil -extract
ApplicationIdentifierPrefix.0, plutil -replace application-identifier)
resolving the wildcard <TEAM>.* -> <TEAM>.<bundle_id>.
- Real codesign with --entitlements when present.
- Asset dirs (add_asset_dir): recursive cp -R src/. into <bundle>/dest/.
Missing src is treated as "nothing to do" so projects can register
add_asset_dir("assets", "assets") unconditionally.
Parser:
- parseStmt() now accepts #import \"path\"; and #framework \"Name\"; as
statement-position tokens. Needed for top-level
inline if OS == .android { #import \"modules/platform/android.sx\"; }
blocks (issue-0042 flatten pass surfaces them); chess's
inline-if-with-#import was rejected at parse time before this fix.
Removals from src/target.zig:
- createBundle, embedFramework, extractEntitlements, buildInfoPlist,
codesign (~210 lines). main.zig no longer calls createBundle after
link(); the sx callback is the single entry point.
Tests / regression markers (all run under sx run host JIT):
- examples/115-post-link-callback.sx — callback registration round-trip.
- examples/116-fs-roundtrip.sx — fs.write_file -> fs.read_file -> exists.
- examples/117-process-roundtrip.sx — process.run + env + find_executable.
- examples/118-macos-bundle.sx — macOS .app via bundle_main callback.
- examples/119-interp-cast-ptr-cmp.sx — cast(T) val under interpreter.
- examples/120-interp-variadic-any.sx — variadic ..Any indexing in IR
interpreter.
- examples/121-ios-sim-bundle.sx — iOS-sim cross-compile + .app with
iOS-shaped Info.plist (added to tests/cross_compile.sh as the
ios-sim tuple).
- examples/122-ios-device-bundle.sx — iOS device cross-compile +
full codesign pipeline (provisioning embed + entitlements
extraction + --entitlements codesign). Manually verified end-to-end:
installed via xcrun devicectl device install app + launched
successfully on iPhone 17 Pro.
- examples/123-inline-if-import-in-body.sx — locks in the parser fix.
zig build && zig build test && bash tests/run_examples.sh => 141 passed,
0 failed; bash tests/cross_compile.sh => 7 passed, 0 failed.
|
||
|
|
cc29cfa7ce |
ffi #jni_main: jni_java_emit + android.sx + manifest fixes; chess on Pixel
Combined slice — gets chess rendering on a Pixel 7 Pro via the
`#jni_main` pipeline. Half-dozen jni_java_emit fixes plus the rebuilt
stdlib android module:
jni_java_emit:
- `#implements Alias;` body members render as Java `implements`
clauses on the class header (space-separated, registry-resolved).
- Drop the implicit `super.<method>(args)` call from the @Override
delegate — interface impls (SurfaceHolder.Callback) have no
super; user calls super explicitly from sx-side via
`super.method(args)` lowered to `CallNonvirtual<T>Method`.
- `static { System.loadLibrary("<libname>"); }` static init block,
lib name derived from the build's `-o` basename.
- `name: Type;` body items render as private Java fields.
- `$` (JNI nested-class shape) → `.` in Java source: e.g.
`android/view/SurfaceHolder$Callback` → `android.view.SurfaceHolder.Callback`.
- Non-void @Override bodies `return` the native delegate's result.
lower.zig:
- `super.method(args)` sugar inside a `#jni_main` (or any
sx-defined `#jni_class`) bodied method lowers to JNI
`CallNonvirtual<T>Method` with the parent class resolved via
`#extends` (default Activity).
- `Alias.new(args)` constructor sugar lowers to JNI
`FindClass + GetMethodID("<init>", sig) + NewObject`.
- `jniMapParamType` stops erasing pointer types so method dispatch
on foreign-class params (`holder.getSurface()`) resolves.
- synthesizeJniMainStub pushes the env arg onto the lexical
`#jni_env` stack so omitted-env `#jni_call` and `super.method`
sites see it.
target.zig:
- Manifest synthesised from `#jni_main` adds
`android:theme="@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"`
so sx apps own the whole window (no title strip, no status bar).
library/modules/platform/android.sx (NEW):
- Replaces the retired NativeActivity-based module under #jni_main.
- Foreign-class decls for Bundle / Context / Surface / SurfaceHolder
/ SurfaceView / MotionEvent / View / Activity / SurfaceHolderCallback /
AssetManagerJ.
- libandroid / EGL / pthread foreign C decls.
- Helpers consumers call from their Activity body:
`sx_android_forward_assets(env, ctx)`,
`sx_android_attach_window(env, holder)`,
`sx_android_detach_window()`,
`sx_android_set_viewport(w, h)`,
`sx_android_start_render_thread(main_fn)`,
`sx_android_push_touch(action, x, y)`.
- Render thread brings up EGL on the ANativeWindow then calls the
user-supplied entry fn pointer.
- `AndroidPlatform` struct + `impl Platform` (init / begin_frame /
end_frame / poll_events / safe_insets / keyboard / show_keyboard /
hide_keyboard / stop / shutdown / run_frame_loop).
End-to-end verified on a Pixel 7 Pro: chess APK builds via
`sx build --target android --apk ... --bundle-id ... -o ...`, installs
via `adb install -r`, launches and renders the chess board with all
pieces in starting position. No title strip, no flicker. Touch events
reach `sx_android_push_touch` and drain through `poll_events` (debug-
verified) — chess's pipeline-side hit-test routing + DPI-correct
sizing remain as follow-ups.
138 host / 8 cross / `zig build test` all green.
|
||
|
|
619d524bac |
ffi #jni_main R.5: retire legacy NativeActivity surface
Deletes the entire NativeActivity / native_app_glue / ALooper stack
that the previous Android entry path was built around:
- `examples/99-android-egl-clear.sx` — the demo of the legacy path.
- `library/modules/platform/android.sx` — `AndroidPlatform.init`,
`run_frame_loop`, `sx_android_bootstrap`, `g_android_app`, plus
the ALooper / AInputEvent / ANativeActivity / AConfiguration
foreign decls that fed them. The JNI helpers (`sx_load_javavm_fn`,
`sx_android_get_env`, `sx_query_safe_insets_jni`, the
`ANATIVEACTIVITY_*` offsets) were tied to the ANativeActivity*
delivered to `android_main` — they're stale now that the OS hands
sx code a Java Activity directly via `onCreate(JNIEnv*, jobject)`.
- `library/vendors/sx_android_jni/sx_android_jni.c` — the input-
handler installer (`sx_android_install_input_handler`), which
poked NDK app-pointer field offsets that no longer exist.
`library/modules/platform/android_jni.sx` (the `Activity`/`Window`/
`View`/`WindowInsets` `#jni_class` registry used for safe-insets
dispatch) survives — it's standalone declarative bindings useful from
any `#jni_main` onCreate body. Docstring updated to drop the
"imported from android.sx" framing.
131 host / 4 cross / zig build test all green. End-to-end smoke APK
still produces the expected JNI-mangled symbol +
SxApp-extends-Activity dex.
External consumers (chess) will need to migrate their entry from the
`AndroidPlatform.run_frame_loop` model to the `#jni_main` model
(Java-side Activity drives lifecycle; onSurfaceChanged / Choreographer
drive frames via JNI callbacks). That migration is downstream work.
|
||
|
|
6a3260ff65 |
ffi 2.16c green: TL fallback via C-helper runtime + always-omit env in #jni_call
`#jni_call` collapses to a single surface — env is *always* implicit:
either picked up from the lexically-enclosing `#jni_env(env) { ... }`
block's Ref (cheap, register-resident, no TL touch) or from the
runtime's thread-local slot via `sx_jni_env_tl_get()` (one fn call
per dispatch). The explicit-env shape is gone — chess and the
existing tests migrate cleanly by wrapping their helper-fn bodies
in `#jni_env(env) { ... }`.
The TL slot lives outside the user's IR module so the LLVM ORC JIT
can load object files cleanly without `orc_rt` for TLS support:
library/vendors/sx_jni_runtime/sx_jni_env_tl.c:
static _Thread_local void *sx_jni_env_tl_slot;
void *sx_jni_env_tl_get(void) { return sx_jni_env_tl_slot; }
void sx_jni_env_tl_set(void *env) { sx_jni_env_tl_slot = env; }
Linkage:
- sx-the-compiler links the .c file via build.zig so the JIT
process-symbol generator resolves `sx_jni_env_tl_get`/`_set`.
- AOT targets get the same .c file auto-linked via the lowering
pass: when lower touches the TL externs, it sets
`needs_jni_env_tl_runtime`, and `Compilation.lowerToIR` appends a
synthetic `CImportInfo` to `lowering_extra_c_sources` that
`collectCImportSources` merges with user-written ones.
Lowering-side changes:
- `getJniEnvTlFids` lazily declares the two externs (parallel
to `getSelRegisterNameFid`) and flips `needs_jni_env_tl_runtime`.
- `#jni_env(env) { body }` emits save→set→body→restore via three
`call` ops to the externs; the inner body sees env via the
lexical-direct stack.
- `lowerJniCall` resolves env from `jni_env_stack` (top) or the TL
fallback. The explicit-env branch is gone.
- `jni_env_stack_base` tracks per-fn lexical scope so lazy-lowering
a callee doesn't accidentally see the caller's Ref (Refs are only
valid inside one fn's instruction stream).
Test migration (mechanical):
- ffi-jni-call-{01..09}: each helper fn wraps `#jni_call(...)`
bodies in `#jni_env(env) { ... }`. Returning values pass through
the block as an expression — `#jni_env` now also lowers in
expression position.
Verified:
- zig build test + tests/run_examples.sh: 130/130 green.
- tests/cross_compile.sh: 3/3 green.
- Chess APK rebuilt + reinstalled on Pixel. Board renders with
status-bar clearance + info panel intact; no crashes in logcat.
Safe-insets dispatch through `#jni_env` + lexical-direct now
fully exercised end-to-end on real hardware.
|
||
|
|
8d1816018a |
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.
|
||
|
|
60f3ffed46 |
ffi 2D: migrate android.sx safe-insets to declarative #jni_class blocks
The four foreign-class declarations move into a new sub-module
`library/modules/platform/android_jni.sx`, imported under a named
namespace from `android.sx`:
Jni :: #import "modules/platform/android_jni.sx";
This keeps the bare class names (`Activity`, `Window`, `View`,
`WindowInsets`) out of the top level — consumers that flat-import
`modules/platform/android.sx` no longer see `View` collide with
`modules/ui/view.sx`'s protocol of the same name (chess hit this
on the first build attempt).
Compiler-side change: `scanDecls`/`lowerDecls` now also iterate any
`namespace_decl` they encounter and register the contained
`foreign_class_decl`s under their qualified name (`Jni.Activity`).
The recursive scan continues to register the bare names too, so
cross-class refs inside method signatures (e.g. `getWindow ::
(self: *Self) -> *Window`) still resolve through the bare key.
Receiver types like `*Jni.Activity` now route through
`getStructTypeName` → "Jni.Activity" → `foreign_class_map` lookup.
`sx_query_safe_insets_jni`'s param signature changes from
`activity: *Activity` to `activity: *Jni.Activity`; the caller in
`AndroidPlatform.safe_insets` casts via `xx`.
Verified on-device — chess APK built with the new sx, installed via
`adb install -r`, launched on the Pixel. Screencap shows the board
rendering with correct status-bar clearance (time + battery icons
visible above the board, board sized below them) — safe insets are
being queried via the new declarative dispatch and produce the same
values as the pre-migration hand-rolled #jni_call chain.
129/129 examples + cross_compile 3/3 + on-device chess all green.
|
||
|
|
c9db2a8dc0 |
ffi 2D: migrate android.sx safe-insets to declarative #jni_class blocks
`sx_query_safe_insets_jni`'s body — previously seven hand-rolled
`#jni_call` sites with verbose JNI descriptor literals — now uses
four `#jni_class` declarations and the DSL method-call form inside
a `#jni_env(env) { ... }` scope. The new shape:
```
WindowInsets :: #jni_class("android/view/WindowInsets") {
getSystemWindowInsetTop :: (self: *Self) -> s32;
...
}
... Activity / Window / View ...
#jni_env(env) {
window := activity.getWindow();
decor := window.getDecorView();
insets := decor.getRootWindowInsets();
top.* = insets.getSystemWindowInsetTop();
...
}
```
Descriptor derivation happens at lower time (jni_descriptor.zig);
slot interning + vtable dispatch shape match the Phase 1C hand-rolled
form byte-for-byte. The function param signature changes from
`activity: *void` to `activity: *Activity` so the DSL can resolve
method names through `foreign_class_map`; the AndroidPlatform.safe_insets
caller adds an `xx` cast at the call site.
Net body shrinks from 14 dispatch lines to 12 (slightly shorter but
the win is type safety + readability — the foreign descriptor
strings are gone). On-device chess regression is the remaining
verification step (Pixel device with safe-area-driven board layout).
Verified locally: zig build, run_examples (129/129), cross_compile
(3/3 — incl. examples/99-android-egl-clear.sx cross-compile to
android target succeeds and produces a valid .o).
Naming caveat: `Activity` / `Window` / `View` / `WindowInsets` are
now top-level names exported by `modules/platform/android.sx`. User
code that imports this module shouldn't redefine these aliases.
|
||
|
|
4ddee931b5 |
ffi 1.29: retire the C sx_android_query_safe_insets body
Closes the Phase 1D migration for the safe-insets JNI chain. The C function and its `#foreign` declaration in `android.sx` are gone; all dispatch now goes through the sx-side `#jni_call` machinery plus the JavaVM helpers landed in 1.26. What's gone from `library/vendors/sx_android_jni/sx_android_jni.c`: - `#include <android/native_activity.h>` and `<jni.h>` (no longer needed without the JNI body). - `sx_android_query_safe_insets` — 55 lines of `(*env)->Foo` chain with manual `goto done` early-exit. Migrated to `library/modules/platform/android.sx::sx_query_safe_insets_jni` in 1.25 (15 lines of `#jni_call`). What stays: - `sx_android_install_input_handler` — non-JNI; struct-field assignment against `struct android_app`'s `onInputEvent` slot. No sx equivalent yet (would need to either land a `#android_app`- style intrinsic or hand-roll the offset, neither of which is Phase 1 scope). - `<android/input.h>` and the `struct sx_android_app_min` mirror needed by the input-handler installer. Net diff: -55 lines in the .c file, -1 line `#foreign` decl in android.sx. Phase 2 (declarative JNI imports) will revisit whether the .c file can be deleted entirely (the input-handler hop may move into a different shape). Verification: - zig build + zig test + run_examples + cross_compile all green. Notable: the previously-failing `ffi-objc-call-12-rect-u64-returns` also passes now — looks like the working-tree `#import c` work was tidied up alongside. - chess Android APK rebuilt + reinstalled + launched on Pixel device; safe-insets behavior unchanged (board top edge sits below the status bar correctly, all pieces in starting positions, no status-bar overlap). |
||
|
|
6e65324f44 |
ffi 1.27: switch safe-insets call site to sx-side JNI implementation
`AndroidPlatform.safe_insets` now reaches into the JVM through the
sx helpers from 1.25 + 1.26 instead of the C `sx_android_query_safe_insets`
foreign call:
attached := false;
env := sx_android_get_env(g_android_activity, @attached);
if env != null {
clazz := sx_android_activity_clazz(g_android_activity);
sx_query_safe_insets_jni(env, clazz, @t, @l, @b, @r);
if attached { sx_android_detach_env(g_android_activity); }
}
Chess Android IR now includes the seven `(@SX_JNI_CLS_*, @SX_JNI_MID_*)`
slot pairs (one per unique literal `(name, sig)` pair: getWindow,
getDecorView, getRootWindowInsets, getSystemWindowInset{Top,Left,
Bottom,Right}). First call populates each; subsequent calls hit the
cached jmethodID via the 1.17 lazy-init branch.
The C `sx_android_query_safe_insets` body is now unused; left in place
per the plan ("leave the file in place until Phase 2 deletes it").
Chess Android + iOS-sim both compile clean; host 118/119;
cross-compile 3/3.
On-device chess regression is the next checkpoint — the safe-area
behavior is visible: board must sit below the status bar with
correct top inset on a Pixel 7 Pro with notch. Deferred to the next
session (requires APK build + adb install + screencap).
|
||
|
|
885b4239c9 |
ffi 1.26: hand-roll JavaVM dispatch in sx for env attach
Adds the JavaVM-side vtable indirection to `library/modules/platform/ android.sx` so the sx caller of `sx_query_safe_insets_jni` (1.25) can obtain a `JNIEnv*` without the C wrapper. `#jni_call` only dispatches through `JNIEnv*`'s vtable (a different table from `JavaVM*`'s), so the JavaVM hop is hand-rolled here. New decls: - `JNI_VERSION_1_6` (0x00010006) and the `ANATIVEACTIVITY_*` byte offsets (8, 24 on 64-bit Android — vm, clazz respectively). - `sx_load_ptr_at(base, offset)` — load a `*void` field at a raw byte offset. Used for both ANativeActivity fields and the JavaVM vtable load. - `sx_load_javavm_fn(vm, slot)` — load function pointer at the given vtable slot. `vm` is `JavaVM*` which points to `JNIInvokeInterface*`; the indirection is `*vm + slot * 8`. - `sx_android_get_env(activity, out_attached)` — calls `GetEnv` (slot 6); on `JNI_EDETACHED` falls through to `AttachCurrentThread` (slot 4), sets `out_attached = true` so caller can balance with `sx_android_detach_env` (slot 5). - `sx_android_activity_clazz(activity)` — reads the jobject at byte offset 24. Chess Android + iOS-sim builds still clean; cross-compile 3/3 green; host 118/119. The new functions dead-strip until step 1.27 wires them into the safe-insets call site in `android.sx::AndroidPlatform.safe_insets`. |
||
|
|
ba0a1a13e3 |
ffi 1.25: sx-side reimplementation of safe-insets JNI chain
Phase 1D for `library/vendors/sx_android_jni/sx_android_jni.c` starts here. Adds `sx_query_safe_insets_jni` to `library/modules/platform/ android.sx` — a sx-side implementation of the JNI dispatch chain that lives inside the C `sx_android_query_safe_insets` helper. The C version is ~50 lines of `(*env)->GetMethodID` + `CallObjectMethod` + `CallIntMethod` boilerplate with manual `goto done` early-exit plumbing on every step. The sx version collapses to four `#jni_call(*void)` chain steps + four `#jni_call(s32)` reads at the end — each #jni_call internally handles GetObjectClass + GetMethodID + Call<Type>Method via the slot interning from 1.17. Signature differences from the C version: - The sx version takes `env: *void` directly. The C version derives it from `ANativeActivity*` via JavaVM's GetEnv/AttachCurrentThread. Bridging that gap (sx-side JavaVM dispatch OR a tiny C shim that returns the env) is the next Phase 1D step. - The activity arg here is the jobject (`ANativeActivity*.clazz`) rather than the activity pointer itself. No call sites switched yet. Chess Android still uses the foreign C function. Cross-compile + chess both targets all clean — verifies the new function typechecks and lowers, but on-device runtime verification is deferred to the integration commit. |
||
|
|
56f6ae3681 |
ffi 1.33: uikit.sx final sweep — Phase 1D for uikit.sx complete
Six remaining dispatch clusters migrated in one pass:
- `uikit_setup_renderbuffer`: `renderbufferStorage:fromDrawable:` (BOOL).
- `uikit_present_renderbuffer`: `presentRenderbuffer:` (BOOL, every frame).
- `uikit_gl_view_tick`: `targetTimestamp` and `duration` reads (f64,
every frame — three call sites total across the keyboard-anim path
and the frame-closure path).
- `uikit_compute_layer_pixel_size`: `bounds` (CGRect HFA).
- `uikit_touch_location`: `locationInView:` (CGPoint HFA — first
standalone `#objc_call(CGPoint)` exercise, structurally identical to
the 2×f64 NSPoint already verified by ffi-objc-call-05).
- `uikit_first_touch`: `anyObject` (*void).
Net -15 lines. uikit.sx is now 839 lines — Phase 1D started at 937,
so this is -98 cumulative across the migration. Zero `xx objc_msgSend`
typed casts left in the file.
iOS-sim chess regression smoke: launched chess, tapped a black pawn
through the Simulator window, watched the move (d7→d5) play, then a
second tap played d5→d4. The render loop, touch handlers, layout
math, and the BOOL-returning EAGL presentation calls are all on the
exercised path, so this is the strongest runtime verification any
Phase 1D commit has had so far.
22 `sel_registerName` calls remain in the file, all legitimate:
- `class_addMethod` IMP registrations (runtime class build-out).
- SEL-as-arg to dispatch selectors that take a SEL value
(`addObserver:selector:name:object:`,
`displayLinkWithTarget:selector:`). A future `#objc_selector("foo")`
literal would replace these, but it's not part of Phase 1.
|
||
|
|
e1d300c661 |
ffi 1.32: uikit_keyboard_will_change_frame via #objc_call
The keyboard notification callback. First standalone exercises of `#objc_call(CGRect)` (HFA — structurally equivalent to UIEdgeInsets, already verified by 1.25 and ffi-objc-call-07) and `#objc_call(u64)` (LLVM-equivalent to s64; ffi-objc-call-04 already locks in the i64 return path). Migrates: - `userInfo` (*void) - `objectForKey:` with NSString arg (*void) - `CGRectValue` (CGRect HFA) - `doubleValue` (f64) - `unsignedLongValue` (u64) - `screen` (*void) - `bounds` (CGRect HFA) Net -14 lines. uikit.sx now 854 lines (-83 cumulative across Phase 1D). iOS-sim chess regression smoke: launch is clean; the callback is registered through cluster 1.30's notification-center wiring and the function lowers without IR-verifier complaints. The callback body itself isn't exercised at runtime by chess startup (the game doesn't open the soft keyboard) — runtime verification of this specific function is transitive via the other clusters that exercise the same call shapes. |
||
|
|
b3558c3274 |
ffi 1.31: uikit_scene_will_connect_ios via #objc_call
The biggest Phase 1D cluster: the iOS scene-lifecycle entry that runs at every launch. UIWindow alloc/init, UIViewController alloc/init, GL view alloc/init/install, root-view-controller wiring, layer access + setOpaque:, EAGL drawable-properties dictionary build, screen/nativeScale DPI scaling, makeKeyAndVisible, UITextField subview install, CADisplayLink construct + addToRunLoop. Every return shape this file uses (void, *void, f64) and every arg shape (BOOL via `xx 0`/`xx 1`, multi-arg selectors `displayLinkWithTarget:selector:` and `setObject:forKey:`) is exercised by this single launch. Net -44 lines on this commit (104 → 60). Also drops a stale `EAGLContext := objc_getClass(...)` decl that wasn't referenced inside this function — EAGL context creation lives in uikit_create_gl_context (already migrated in 1.29). uikit.sx is now 868 lines (-69 cumulative across Phase 1D). iOS-sim chess regression smoke: app launches cleanly, board renders with status-bar clearance, sharp DPI scaling, compositor working, display-link tick driving frames. Every part of the migrated function is on the launch path and all of it succeeds. |
||
|
|
ee53348ce0 |
ffi 1.28 follow-up: keyboard BOOL returns use #objc_call(bool)
Apple documents `-becomeFirstResponder` and `-resignFirstResponder` as returning `BOOL`. The pre-`#objc_call` cast pattern in this file used `u8` because BOOL is ABI-equivalent to a 1-byte unsigned integer on both i386 (signed char) and arm64 (`bool`). The initial 1.28 migration carried that `u8` typing forward without question; switching to `bool` matches the documented API and aligns with the BOOL→bool mapping called out in PLAN-FFI.md Phase 3. First standalone exercise of `#objc_call(bool)`. The lowering is identical to `#objc_call(u8)` at the ABI layer (single byte in `w0` on AAPCS64), but the source-level type is now meaningful. |