Commit Graph

24 Commits

Author SHA1 Message Date
agra
6b0ebdd92b lang: require explicit receiver in protocol method declarations
Protocol method declarations now declare their receiver explicitly as the first
parameter — 'self: *Self' (or 'self: Self') — matching the impl method signature,
instead of the old implicit-receiver form where the listed params were only the
extra args. That asymmetry repeatedly caused confusion over whether the first
param was the receiver or an argument.

The parser validates the first param is 'self' typed Self/*Self, then strips it,
so all downstream lowering and the dispatch ABI are unchanged (impl blocks and
call sites are unaffected). A protocol method missing the receiver is now a parse
error.

Migrated all 129 protocol method signatures across library + examples (+ one
inline-sx test in sema.zig) to the explicit form. Updated specs.md + readme.md.

New: examples/0418-protocols-explicit-receiver.sx (feature),
examples/1190-diagnostics-protocol-missing-receiver.sx (negative/diagnostic).
2026-06-21 11:02:16 +03:00
agra
b06776d6e9 library: vendors/kb_text_shape + vendors/file_utils; modules/ffi/stb_truetype.sx retired
kb_text_shape (v2.10, JimmyLefevre) had been LOST from the sx tree —
ffi/stb_truetype.sx referenced repo paths that no longer existed (and
nothing runs glyph_cache, so the dangling unit never fired). The
trimmed copy returns from the m3te project as a proper vendor:
curated c/kbts_api.h decls over the full upstream header, README with
provenance, and examples/1627 pinning context + font creation so the
unit compiles and runs in-suite. file_utils (in-house asset-read
helper with the Android AAssetManager hook) gets the same unit shape.

modules/ffi/stb_truetype.sx is gone: glyph_cache imports the three
vendored units (stb_truetype, kb_text_shape, file_utils) directly.
2026-06-12 17:58:23 +03:00
agra
58af806b7a library: vendors/stb_image + vendors/stb_truetype — stb ships with sx
The stb headers move from the repo-root vendors/ (resolvable only
with CWD = sx repo) into library/vendors/ following the sqlite
convention — bindings module + c/ sources + provenance README — so
'#import "vendors/stb_image/stb_image.sx"' (image v2.30 + image_write
v1.16) and '#import "vendors/stb_truetype/stb_truetype.sx"' (v1.26)
work from any consumer via the stdlib search paths. modules/ffi/stb.sx
dissolves into the stb_image vendor; modules/ffi/stb_truetype.sx keeps
its non-stb text-shaping companions and re-imports the vendored unit.
examples/1625 pins a deterministic in-memory BMP decode; examples/1626
pins font init + metric invariants against the system Helvetica.
2026-06-12 17:50:21 +03:00
agra
d8076b9333 lang: rename signed integer types sN -> iN
Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.

Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).

Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.

zig build test: 426/426; examples suite: 595/595.
2026-06-12 09:31:53 +03:00
agra
88bae3c9f5 mem: rename Allocator primitives to alloc_bytes/dealloc_bytes (Phase 4 naming pulled forward, Agra-approved) 2026-06-11 15:33:35 +03:00
agra
12bf61a9fc std: restructure step 3 — ffi/ moves, build.sx, math dir spelling, fixtures
- objc.sx, objc_block.sx (from std/) + sdl3/opengl/raylib/stb/stb_truetype/
  wasm vendor bindings (from modules/ root) -> modules/ffi/
- std/uikit.sx deleted: platform/uikit.sx already declares UIApplicationMain
  and imports objc; '#framework "UIKit"' cannot live in a file imported on
  macOS targets (unconditional link directive, UIKit is iOS-only), so the
  three iOS-only examples carry the 3-line glue inline. 1607/1608/1616 also
  un-rotted (dead ns_string -> 'xx "..."' Into conversions, callconv(.c)
  msgSend fn-ptrs) — all three build for ios-sim/ios again.
- math/math.sx -> math/scalar.sx; one spelling '#import "modules/math"'
  everywhere (4 pinned IR snapshots regenerated: dir import adds Vec2/Mat4
  to the type tables).
- compiler.sx -> build.sx (imports, CLAUDE.md bundling table, specs.md).
- testpkg/ + test_c.sx -> tests/fixtures/ (resolve CWD-relative from repo
  root, same as vendors/).
- library-internal imports use full modules/... paths (std.sx tail,
  platform/bundle.sx, fixtures).
2026-06-11 08:37:22 +03:00
agra
59f0aa7716 std: restructure — std/ modules, namespace tail, std/xml.sx
allocators/fs/process/socket/log/trace/test move under modules/std/
(allocators.sx becomes std/mem.sx; the Allocator protocol moves into
the std.sx prelude, impls stay in mem.sx). New std/xml.sx holds
xml_escape as xml.escape. std.sx gains the carried namespace tail —
flat-importing std.sx now also provides mem./xml./log. — with the
remaining modules (fs/process/socket/json/cli/hash/test) deferred from
the tail until the global last-wins maps are fully own-wins (pulling
them into every closure collides bare names corpus-wide; they stay
direct imports: modules/std/fs.sx etc.). log.sx's internal emit
renamed log_emit (it clobbered consumer fns named emit program-wide).
bundle.sx uses xml.escape via the carried alias. Consumer import paths
swept mechanically; .ir snapshots recaptured for the larger std
closure. m3te + game build unchanged.
2026-06-11 06:10:59 +03:00
agra
33a6f5c650 wip(E4): partial source-pin + non-transitive flip [stdlib E4 attempt-1 WIP checkpoint]
Incomplete WIP from a worker killed at the 55-min wall (large blast radius:
core source-pin + ~8 example migrations + ~10 library module migrations).
Committed so the resumed session continues on a clean tree. May not build.
2026-06-08 11:12:08 +03:00
agra
df6e830bec fix(diagnostics): reject reserved type-name bindings in every module (issue 0077)
The issue-0076 reserved-type-name binding diagnostic only ran over main-file
decls, so an imported module (or the stdlib) could still declare `s2 := ...`
and reach lowering, where the address-of family loads the whole aggregate and
passes it by value to a `ptr` param — LLVM verifier abort.

Extend coverage to every compiled module: a dedicated `checkBindingNames` walk
(in semantic_diagnostics.zig) visits every var/`:=`/typed-local binding name and
function/lambda/struct-method parameter at any depth, with NO main-file filter,
descending the `namespace_decl` that a `mod :: #import` wraps so imported-module
decls are reached. It tracks each module's source_file (save/restore per node)
so the diagnostic renders against the imported module's text. Rejection still
defers to the parser's `Type.fromName` classifier; the unknown-type check (0064)
stays main-file-only. No lowering special-case; `.identifier`-only address-of
paths are unchanged.

Stdlib audit: the only reserved-name bindings under library/ were two `u1`
locals in ui/renderer.sx (UV coords) — renamed to u_min/u_max/v_min/v_max.

Regression test: examples/1120-diagnostics-imported-reserved-type-name.sx (+
companion mod.sx) — an imported `s2 := ...` now emits the clean diagnostic at
the import's declaration site (exit 1), not an LLVM abort.

Resolves issues 0076 (coverage extension) and 0077.
2026-06-03 19:32:49 +03:00
agra
bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

Compiler:
- Parser records `Block.produces_value` (last stmt is a no-`;` trailing
  expression) + `Block.discarded_semi` (the `;` that discarded a value),
  via `expectSemicolonAfter`. A trailing expression before `}` may now
  omit its `;` (previously a parse error). Match-arm and else-arm bodies
  are built value-producing regardless of the arm `;` (arms are exempt —
  the `;` is an arm terminator).
- Lowering: `lowerBlockValue` / the block-expr path / `inferExprType`
  respect `produces_value`. A value-position block that discards its value
  is a hard error (`lowerValueBody` for function bodies; the value-context
  `.block` path for if/else branches, `catch` bodies, value bindings,
  match arms). Pure-failable `-> !` bodies (value rides the error channel)
  and a value-if whose branches are void are handled without false errors.
- `defer`/`onfail` cleanup bodies lower as statements (void), so a
  trailing `;` there is fine.

Migration (behavior-preserving — output unchanged):
- stdlib + ~210 examples: dropped the trailing `;` on value-position last
  expressions. `format` now ends with an explicit `#insert "return
  result;"` (it relied on `#insert`-as-block-value, which `;` discards).
- Two `main :: () -> s32` examples that relied on the old silent
  default-return got an explicit trailing `0`.
- Rejection snapshots 0412 / 1013 regenerated (their quoted source lines
  lost a `;`); the diagnostics themselves are unchanged.

Docs/tests: specs.md "Block values" section; examples 0040 (rules) + 0041
(rejection); 3 parser unit tests. Filed issue 0066 (pre-existing
match-arm negated-literal phi-width quirk, surfaced not caused here).

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00
agra
c08433b345 ui: drop redundant .* on pointer match subjects
event_position and translate_sdl_event matched on e.* / sdl.*; lowerMatch now auto-derefs a pointer subject, so 'if e ==' / 'if sdl ==' are equivalent (same load + tag-switch in IR). Pure cleanup.
2026-05-31 10:46:51 +03:00
agra
cd2ab1c4b6 ui: platform-neutral Keycode enum; map SDL keycodes once
KeyData.key was a raw u32 carrying SDL_Keycode values, so app code had to reinterpret it as SDL_Keycode (xx e.key) — a leaky, unchecked cross-platform cast only valid because the backend happened to be SDL. Add a neutral Keycode enum; translate_sdl_event maps SDL_Keycode to it via keycode_from_sdl. App code compares e.key == .escape with no platform type and no cast; a new backend maps its own native codes in one place.
2026-05-31 10:42:55 +03:00
agra
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`.
2026-05-25 15:33:28 +03:00
agra
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`.
2026-05-25 14:41:17 +03:00
agra
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.
2026-05-24 22:59:20 +03:00
agra
5c41e9c180 gpu: Gles3Gpu — GLES3 implementation of the GPU protocol
Mirror of metal.sx, talks to GLES3 via opengl.sx's runtime-loaded
fn-pointer variables. EGL bootstrap is owned by AndroidPlatform; this
module just calls `load_gl(@eglGetProcAddress)` once during `init` to
populate the pointers, then drives raw draw/state from there.

The renderer's vertex layout (12 floats: pos2/uv2/color4/params4 = 48
bytes, attribute locations 0-3) is hardcoded in a single shared VAO
the Gles3Gpu owns — `set_vertex_buffer` rebinds the active VBO against
it. `set_vertex_constants(slot=1, data, 64)` is treated as the 4x4
projection matrix; `set_texture(slot=0, ...)` binds texture unit 0 and
sets `uniform sampler2D uTex` — both match renderer.sx's shader
contract.

A subtle gotcha caught + recorded in the file header: declaring the
same GL name as a `#foreign` function while opengl.sx also declares it
as an fn-pointer global silently lets the global win, and calling
through the uninitialized variable jumps to PC=0. Solution: don't
re-declare; use opengl.sx's pointers and `load_gl` them.

renderer.sx: the GPU-protocol shader-source branch now passes
(UI_VERT_SRC_ES, UI_FRAG_SRC_ES) on Android (separate vert+frag) vs.
the combined MSL library on iOS. Both gated with `inline if OS == X`.
2026-05-19 09:32:09 +03:00
agra
f41a121a29 gpu: destroy_shader/buffer/texture on the GPU protocol (issue-0029)
Three new method signatures on the GPU protocol. Metal backend sends
`release` to the MTLTexture/Buffer/RenderPipelineState and nulls the
slot in its backing List so the handle becomes inert; handles are not
re-used. glyph_cache.grow() now destroys the old atlas before
allocating its replacement, eliminating the per-grow leak the file's
comment had been flagging since Session 62.
2026-05-18 23:09:32 +03:00
agra
79419b99bd issue-0028: ?Protocol = null sentinel-shaped optional protocols
Protocol structs registered via registerProtocolDecl carry a new
is_protocol flag; the ?T paths in sizeOf/typeSizeBytes/toLLVMType
recognise it and lay out ?Protocol as the protocol struct itself
(ctx == null IS the "none" state), matching how ?Closure / ?*T are
sentinel-shaped — no extra storage.

Method dispatch on ?Protocol auto-unwraps in lowerCall's field-access
path; the unwrap is structurally a no-op so we just rebind obj_ty to
the payload type. resolveCallParamTypes extended for optional-protocol
receivers so enum-literal args (gpu.create_texture(.r8, ...)) get the
right target_type and don't silently collapse to tag=0 : s32 — same
issue-0031-class bug closed in Session 66, one type-system layer
deeper.

Library: UIRenderer / UIPipeline / GlyphCache migrated from the verbose
gpu: GPU = ---; has_gpu: bool pattern to gpu: ?GPU = null. set_gpu no
longer maintains a parallel bool flag.

Bundled: dock.sx threads delta_time as a struct field rather than via
a global pointer (cleanup unrelated to issue-0028, committed alongside).

Verified: 85/85 regression tests pass; iOS-sim chess + macOS chess
both render correctly post-migration.
2026-05-18 18:32:55 +03:00
agra
f9ecf9d00e iOS lock step keyboard + metal 2026-05-18 17:40:10 +03:00
agra
b43472e6ab ui: text shader uses raw atlas coverage (no SDF smoothstep)
Two small cleanups in the Metal text path on top of the buffer-offset
fix from cc71d95:

- Drop the SDF-style `smoothstep(0.5 ± ew, alpha)` from the text mode
  branch in UI_MSL_SRC. The glyph atlas stores alpha coverage straight
  from stbtt_MakeGlyphBitmap, not signed distance, so the smoothstep
  was thinning anti-aliased strokes by mapping mid-coverage values
  (0.3–0.7) toward 0/1. Use the sampled value directly as alpha.

- Drop the 16-byte alignment pad on `mtl_buf_offset` in `flush()`. Each
  batch's upload_size is already a multiple of UI_VERTEX_BYTES (48), so
  the running offset stays vertex-aligned without the extra rounding.

- After `font.shape_text` + `font.flush` in `render_text`, re-bind
  `font.texture_id`. If the atlas grew during shaping, the GPU texture
  handle changed; without this rebind the next flush samples the old
  (smaller) atlas which doesn't have the newly-rasterized glyphs.

- Use explicit s64-pointer arithmetic in `metal_update_buffer_at_ios`
  so a future regression in `[*]u8` indexing can't quietly miscompile
  the per-flush write offset.

Text at small sizes still renders dim on dark backgrounds — most glyph
pixels sit in 0.1–0.5 coverage and the linear blend doesn't push them
to bright values — tracked separately as the faint-text follow-up.
2026-05-18 10:26:31 +03:00
agra
cc71d9591d ui: per-flush byte-offset on Metal vertex buffer fixes chess board
UIRenderer.flush wrote to mtl_vbuf at byte offset 0 on every flush.
Metal records draw commands but reads the buffer at GPU execution time,
so a frame with multiple flushes ended up rendering whatever the LAST
writer left in the buffer for every draw. Chess UI hit this hard:
each of the 32 pieces in the initial position triggers two bind_texture
flushes (atlas -> pieces -> atlas), so ~64 mid-frame flushes silently
rendered the final info-panel batch over the board and the sprites.

New GPU protocol method update_buffer_at(buf, data, size, byte_offset);
Metal impl writes at offset via [*]u8 arithmetic on [buf contents].
UIRenderer tracks mtl_buf_offset (reset in begin, advanced per flush,
aligned to 16B, wraps on overflow) and draws each batch with
vertex_off = byte_off / UI_VERTEX_BYTES. Metal buffer over-allocated
4x the per-flush max (~3 MB) for headroom. GL path untouched —
glBufferData already orphans the storage.

71/71 regression tests pass. Metal-clear example, macOS GL chess, and
WASM chess all still build.
2026-05-18 09:19:21 +03:00
agra
a1647eab9b metal: pause step 3b pending sx-side fixes (filed 0024-0030)
Step 3b code is wired across UIRenderer + GlyphCache + UIPipeline +
chess game (gpu_mode = .metal on iOS, MetalGPU bound via the GPU
protocol). macOS GL chess, iOS-sim GLES chess, and iOS-sim Metal
triangle (63-metal-clear.sx) all still render.

iOS-sim Metal chess crashes inside replaceRegion uploading the 1MB
font atlas. Bisecting that crash exposed several sx-language issues
where mid-bisect tracers (NSLog inside if/else branch bodies) didn't
produce output, blocking further investigation.

Filing each finding as examples/issue-NNNN.sx rather than working
around piecemeal:

Bugs:
- 0024 NSLog/foreign-call inside if/else body not producing output
- 0025 C-ABI param coercion incomplete for composites >16B
       (combined direct-call abiCoerceParamType TODO + call_indirect
        path that doesn't apply C-ABI coercion at all)
- 0026 replaceRegion 1MB upload crash (likely downstream of 0025)

Features needed for step 4 + cleanup:
- 0027 Obj-C block bridge (^{...}) for animateWithDuration:
- 0028 Optional protocol box (?GPU = null) replaces T = ---; has_T: bool
- 0029 destroy_texture/buffer/shader on GPU protocol
- 0030 extern cross-file globals

Library-side: renderer.sx + glyph_cache.sx + pipeline.sx gain a
`gpu: GPU = ---; has_gpu: bool` field pair + branches that route every
GL touchpoint through the protocol when has_gpu. glyph_cache.init
saves/restores those fields around its memset. pipeline.set_gpu()
propagates to renderer + font. Renderer's MSL shader source added as
UI_MSL_SRC using packed_float2/packed_float4 to keep the 12-float
interleaved vertex layout tight (48 bytes).

metal.sx: dual-phase init (init(null, 0, 0) for eager device+queue,
re-init with the layer once UIKit installs the SxMetalView).
setStorageMode:.shared on every texture descriptor to ensure CPU-
writable atlas pixels on Apple Silicon iOS-sim.

Regression suite: 68 passing, 0 failed. WASM chess build currently
broken under step 3b state (silent compiler crash); documented in
CHECKPOINT.md, likely fallout from one of the filed issues (probably
0028 — the verbose protocol-box pattern). Step 3b resumes after
0024-0030 land.
2026-05-17 21:17:17 +03:00
agra
4e27a7e6c9 platform: UIKitPlatform end-to-end — chess game runs on iOS sim
What works on iOS sim now:
- pure-UIKit boot via UIApplicationMain (no SDL3 on iOS)
- SxGLView (CAEAGLLayer) + EAGLContext(GLES3) + CADisplayLink
- GLES3 shader path in modules/ui/renderer.sx (was wasm-only; now
  wasm-OR-ios)
- UITouch -> ui.Event translation (mouse_down/moved/up) on touchesBegan/
  Moved/Ended/Cancelled. Verified by tapping the chess board: the
  expected pawn highlights and its legal moves show as green dots.
- chdir to NSBundle.mainBundle.resourcePath inside UIKitPlatform.init so
  the game's relative fopen("assets/...") calls resolve.

Required restructuring to fix four problems discovered along the way:

1. GL context + load_gl must happen BEFORE UIApplicationMain so the
   game's pipeline.init (which compiles shaders) doesn't crash on null
   function pointers. Pulled EAGLContext creation + load_gl out of
   didFinishLaunching: into UIKitPlatform.init via uikit_create_gl_context.

2. UIScreen.nativeScale returns CGFloat (=double on 64-bit Apple).
   Reading it through a `(*void, *void) -> f32` msgSend signature
   clobbers the value to 0 — the upper 32 bits of d0 land where the f32
   reads from. Replaced msg_f with msg_d returning f64 (and added
   msg_odbl for setContentScaleFactor: which takes CGFloat).

3. `xx <f64-call-result>` directly assigned to an f32 field through a
   sema path lowers as `sitofp` (integer→float) on the double — LLVM
   verification rejects it. Workaround: hoist into an `f64` local first.

4. The renderer was selecting the GLSL 330 core shader on every non-wasm
   target, including iOS GLES3 where it silently fails to compile and
   no quads render. Added OS == .ios to the GLES branch.

Game changes:
- main.sx: g_plat is now a boxed `Platform` (not concrete *SdlPlatform).
  Backend chosen per-target via `inline if OS == .ios { ... }`. The
  ESC-to-stop handling is OS-guarded (mobile apps don't quit on key
  press, and SDL_Keycode references would force-link SDL on iOS).
- build.sx: iOS no longer adds SDL3; it adds UIKit + OpenGLES +
  QuartzCore instead.
- delta_time and viewport dims are now mirrored to free globals so the
  dock subsystem (`g_dock_delta_time = @g_delta_time`) and build_ui
  layout decisions don't need a pointer through the boxed protocol.

Other:
- Added `stop()` to the Platform protocol (no-op on UIKitPlatform).
- examples/66-uikit-platform.sx updated: taps advance the clear color
  (red → green → blue) — smoke test for the touch IMP wiring.
- shutdown() on UIKitPlatform is a no-op (mobile apps don't tear down).

Outstanding for next session:
- The Dynamic Island notch overlaps the top of the board because we
  haven't read UIView.safeAreaInsets yet (CGRect/UIEdgeInsets struct
  returns require a different msgSend ABI than we currently express).
- Keyboard observer (UIKeyboardWillChangeFrameNotification + animation
  duration) — the load-bearing iOS feature.
- Real-device codesigning workflow for the new build.

Two more sx compiler bugs to file out of this work:
- xx(f64 call result) → f32 emits sitofp (problem #3 above).
- Inline `#import` inside `inline if` fails to parse (we worked around
  by importing both backends unconditionally; the unused-backend's
  Obj-C calls are gated by `inline if OS == .ios`).
2026-05-17 16:52:03 +03:00
agra
dc8529e3ea ui: port game UI framework into library/modules/ui
20 files (~3,830 lines): view protocol, layout, renderer, glyph cache,
fonts, gestures, animation, scroll, stacks, modifiers, etc.

Internal imports rewritten from "ui/..." to "modules/ui/...".
Consumers now `#import "modules/ui"` from any project; no symlink
hacks needed. Verified by compiling game/main.sx without its local
ui/ — resolves via the Phase 6 stdlib fallback.
2026-05-17 13:54:11 +03:00