Commit Graph

113 Commits

Author SHA1 Message Date
agra
7b566bfb83 ffi 1.23: #jni_static_call lowering — make-green
Static dispatch wired in. The early `is_static` bail in
`.jni_msg_send` is gone; both paths now share the same lazy-cache +
phi structure with two static-specific differences:

1. `GetObjectClass` is skipped — for static calls, `target` IS the
   `jclass`. The cached `cls` slot just stores `NewGlobalRef(target)`
   directly.
2. The method-ID lookup uses `GetStaticMethodID` (slot 113), and the
   dispatch uses `CallStatic<Type>Method` (Object 114 / Boolean 117
   / Int 129 / Long 132 / Float 135 / Double 138 / Void 141).

Slot interning still applies: the `@SX_JNI_{CLS,MID}_<key>` pair is
shared between instance and static literal call sites with the same
`(name, sig)` — though in practice the JNI runtime treats instance
and static method-IDs as distinct, so two sites with the same name
but different dispatch kinds would collide in the cache. This isn't
a problem the chess Android backend hits (each method is uniquely
either static or instance in the API), so the simpler single-key
intern stays.

IR snapshot updated: `ret i32 undef` replaced by the full
NewGlobalRef → GetStaticMethodID → CallStaticIntMethod sequence
through vtable slots 21, 113, 129. Args `i32 3, i32 7` thread through
the existing arg-coercion loop.
2026-05-19 22:43:20 +03:00
agra
b5694cc42d ffi 1.22: #jni_call(*void) → CallObjectMethod (slot 34) — make-green
Closes the return-type matrix. Pointer-return types aren't a simple
`TypeId` enum case (they're user-defined types interned into the
table), so the dispatch checks `TypeInfo.pointer | .many_pointer`
ahead of the primitive switch:

  const is_pointer_ret = switch (types.get(ret_ty_id)) {
      .pointer, .many_pointer => true,
      else => false,
  };
  const offset = if (is_pointer_ret)
      Jni.CallObjectMethod
  else switch (ret_ty_id) { .void => ..., .s32 => ..., ... };

LocalRef cleanup deferred: returned jobjects are JNI LocalRefs
bounded by the native frame. Chains of calls within one frame
consume them inline; cross-frame use must promote via `NewGlobalRef`
(already wired in the slot-interning path from 1.17). The chess
Android backend will consume objects inline, matching the manual
pattern in `sx_android_jni.c`.

Return-type matrix done: void, s32, s64, f64, bool, *void all
dispatch through their respective vtable slots. Static dispatch
(1.23) is next.
2026-05-19 22:38:09 +03:00
agra
b0e8659c2f ffi 1.21: #jni_call(bool) → CallBooleanMethod (slot 37) — make-green
One-line addition: `.bool => Jni.CallBooleanMethod`. The lazy-cache
+ dispatch from 1.17 handles the rest. JNI's `jboolean` is i8 in the
C ABI but always carries 0 or 1; LLVM's call boundary truncates the
return byte to i1 and the sx-level bool reads the low bit
canonically.

IR snapshot updated: `ret i1 undef` replaced by the full sequence
through vtable slot 37 keyed on `("isShown", "()Z")`.
2026-05-19 22:35:20 +03:00
agra
ca4ba7589c ffi 1.20: #jni_call(f64) → CallDoubleMethod (slot 58) — make-green
One-line addition to the switch: `.f64 => Jni.CallDoubleMethod`.
First non-integer JNI return type; same lazy-cache + dispatch
infrastructure from 1.17 handles the rest.

IR snapshot updated: `ret double undef` replaced by the full
sequence through vtable slot 58 keyed on `("getValue", "()D")`.
2026-05-19 22:32:40 +03:00
agra
5945a8c176 ffi 1.19: #jni_call(s64) → CallLongMethod (slot 52) — make-green
One-line addition to the `call_method_offset` switch: `.s64 =>
Jni.CallLongMethod`. The 1.17 caching infrastructure and the named-
constants struct from c1877fc handle the rest.

IR snapshot at `tests/expected/ffi-jni-call-05-jlong-return.ir`
updated: `ret i64 undef` replaced by the full lazy-cache +
CallLongMethod (vtable slot 52) sequence keyed on
`("currentTimeMillis", "()J")`.
2026-05-19 22:30:49 +03:00
agra
c1877fc00e ffi: lift JNI vtable offsets into a named-constants struct
The numeric slot indices (21, 31, 33, 49, 61) in the `#jni_call`
lowering are JNI-spec constants from `<jni.h>` but appeared as bare
magic numbers — only the trailing comment told you which JNI
function you were loading. Moving them into a private `const Jni`
namespace at file scope makes the call sites self-documenting:

  loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass")
  loadJniFn(ifs, Jni.NewGlobalRef,   "jni.NewGlobalRef")
  loadJniFn(ifs, Jni.GetMethodID,    "jni.GetMethodID")
  switch (ret_ty_id) {
      .void => Jni.CallVoidMethod,
      .s32  => Jni.CallIntMethod,
      ...
  }

Also pre-loaded the remaining Call<Type>Method slots (Object,
Boolean, Long, Float, Double) so steps 1.19–1.22 just add the
corresponding switch arm — no new magic-number lookups in the diff.

Behavior-preserving refactor: IR snapshots unchanged, all 113 host
tests still pass, both cross-compile tuples still green.
2026-05-19 22:28:51 +03:00
agra
ebcfe4c4dc ffi 1.18: #jni_call(s32) → CallIntMethod (slot 49) — make-green
One-line addition to the `call_method_offset` switch in
`emit_llvm.zig` — `.s32 => 49` (CallIntMethod). The 1.17 caching
infrastructure handles the rest: GetObjectClass → NewGlobalRef →
GetMethodID populate the shared `@SX_JNI_{CLS,MID}_<key>` pair on
miss; per-call lowering loads the cached jmethodID and dispatches
through vtable slot 49 with an `i32` return.

IR snapshot at `tests/expected/ffi-jni-call-04-jint-return.ir`
updated: the `ret i32 undef` placeholder is replaced by the full
lazy-cache + CallIntMethod sequence keyed on
`("getCount", "()I")`. Pre-1.18 snapshot was 1d7ea72.
2026-05-19 22:26:58 +03:00
agra
0d883b412d ffi 1.17: #jni_call(name, sig) literal-keyed slot interning
Two `#jni_call` sites with the same string-literal `(name, sig)` pair
now share a single `jclass` GlobalRef slot and a single `jmethodID`
slot, populated lazily on the first call to any matching site.
Non-literal sites keep the per-call `GetObjectClass` + `GetMethodID`
sequence from step 1.15.

Per-call-site lowering for literal sites:

  %cached_mid = load ptr, @SX_JNI_MID_<key>
  %is_cached  = icmp ne ptr %cached_mid, null
  br i1 %is_cached, cont, miss
miss:
  %local_cls  = GetObjectClass(env, target)
  %global_cls = NewGlobalRef(env, local_cls)     ; vtable slot 21
  store ptr %global_cls, @SX_JNI_CLS_<key>
  %fresh_mid  = GetMethodID(env, global_cls, name, sig)
  store ptr %fresh_mid, @SX_JNI_MID_<key>
  br cont
cont:
  %mid = phi ptr [%cached_mid, before], [%fresh_mid, miss]
  call <Type>Method(env, target, %mid, args...)

Wiring:
- `JniMsgSend.cache_key: ?CacheKey` (new) carries `(name_str,
  sig_str)` when both `name` and `sig` are string-literal AST nodes;
  empty for non-literal call sites.
- `lower.zig` populates `cache_key` from the AST.
- `emit_llvm.zig` `getOrCreateJniSlots(name, sig)` returns the
  `{cls_slot, mid_slot}` pair, creating and caching them on first
  lookup. Key is `name\x00sig` so the separator can't collide with
  any JNI identifier byte.
- `mangleJniKey` builds an LLVM-identifier suffix from the pair, used
  in the `@SX_JNI_{CLS,MID}_<suffix>` global names.

IR snapshot at `tests/expected/ffi-jni-call-03-methodid-sharing.ir`
updated: two call sites against literal `("noop", "()V")` now share
`@SX_JNI_CLS_noop____V` and `@SX_JNI_MID_noop____V`. Pre-1.17 snapshot
had two independent `GetMethodID` calls; post-1.17 has one global
slot pair plus per-call lazy-init branches.

Note: an unrelated regression in `examples/ffi-objc-call-12-rect-u64-returns.sx`
exists in the working tree (parse error from an in-progress C-import
block) and is left untouched.
2026-05-19 22:22:55 +03:00
agra
9afcaa5af0 ffi 1.15: #jni_call(void) codegen — make-green
New `.jni_msg_send` IR opcode carrying `{env, target, name, sig,
args[], is_static}`. `lowerFfiIntrinsicCall` now dispatches on
`fic.kind`: `.objc_call` keeps the existing path; `.jni_call` and
`.jni_static_call` route through `lowerJniCall`, which emits the new
opcode.

emit_llvm.zig expands `.jni_msg_send` into the JNI vtable
indirection:

  %ifs              = load ptr, %env                  ; vtable
  %get_obj_class    = load ptr, gep(%ifs, i32 31)
  %cls              = call ptr %get_obj_class(%env, %target)
  %get_method_id    = load ptr, gep(%ifs, i32 33)
  %mid              = call ptr %get_method_id(%env, %cls, %name, %sig)
  %call_void_method = load ptr, gep(%ifs, i32 61)
  call void %call_void_method(%env, %target, %mid, args...)

Per step 1.15's scope: only `.jni_call` (instance) + `void` return
are wired through the switch. `.jni_static_call` (1.23) and the
non-void returns (1.18–1.22) drop to a placeholder `LLVMGetUndef` so
the build doesn't fault — the next-step commits flip those arms one
shape at a time. Method-ID caching is step 1.17.

Two small helpers landed alongside:
- `loadJniFn(ifs, offset, name)` — GEP into the vtable + load.
- `extractSlicePtr(val)` — string literals lower as `{ptr, i64}`
  slices in sx IR; JNI's `GetMethodID` expects raw C strings, so
  this extracts field 0 when the source is a slice.

Android cross-compile now passes for `examples/ffi-jni-call-02-void.sx`
(2/2 cross targets green). Host run_examples still passes 112/112.
Chess iOS-sim + Android both compile clean.
2026-05-19 21:32:18 +03:00
agra
df2ccf77bd issue-0038 fixed: closure capture through FfiIntrinsicCall args
`collectCaptures` in `src/ir/lower.zig` was the closure free-variable
analyzer that decides which names from a closure body need to be
boxed into the env struct at lambda-build time. Its switch on AST
node kind enumerated every other shape (`.call`, `.if_expr`,
`.match_expr`, `.for_expr`, etc.) but no arm for `.ffi_intrinsic_call`,
so the trailing `else => {}` quietly dropped its `args[]` and
`return_type` walks. Names referenced inside `#objc_call(T)(recv,
"sel:", ...)` from a closure body never made it into the captures
list, so when lowering bound the closure scope from env, those names
came back as "unresolved".

The fix adds the missing arm — walk `return_type` and every `args[i]`
the same way `.call` walks `callee` + `args`.

Companion changes:
- `examples/issue-0038.sx` → `examples/103-ffi-closure-capture.sx`
  (out of the open-issue namespace; comment header tightened to
  describe the feature, not the historical bug).
- `examples/ffi-objc-call-09-in-construct.sx` drops the
  `g_hasher_recv` module-global workaround that was added for this
  bug — the closure now captures `recv` from `make_hasher`'s arg
  list normally.
2026-05-19 21:14:31 +03:00
agra
0bb7b8cc27 issue-0037 fixed: ptr↔int conversion in coerceToType / bitcast emit
109/109 regression tests pass; chess Android + iOS-sim still
build clean.

Root cause: sx's `xx <ptr>` cast targeting an integer type
(common pattern: `xx u64 = xx @some_global`) lowered to a no-op
because `coerceToType` had branches for int↔float and same-kind
widen/narrow, but nothing for pointer↔integer. The cast left the
value as a pointer Ref, and `emitInst`'s `.ret` arm tried to
coerce a `ptr` value to an `i64` slot — coerceArg had no
ptr↔int branch either, fell through to undef.

Why it worked in main but failed in helpers: an
`alloca u64`+`store ptr @g, alloca`+`load i64, alloca` sequence
preserves the address bits as raw memory, so the
"store-then-load through an alloca" workaround happened to do
the right thing without a real cast. A `ret i64 <ptr>` has no
such intermediate slot and triggers an LLVM type mismatch.

Fix layered into two existing IR opcodes:

  lower.zig (coerceToType):
    new branch — when src and dst types are ptr↔int, emit a
    `bitcast` IR opcode with the right from/to. Mirrors how
    int↔float emits `.int_to_float` / `.float_to_int`.

  emit_llvm.zig (.bitcast arm):
    dispatch ptr→int to `LLVMBuildPtrToInt` (+ trunc/zext if the
    target int width != 64), int→ptr to `LLVMBuildIntToPtr`. The
    "real bitcast" path stays for same-kind type punning.
    Modern LLVM's BuildBitCast rejects ptr↔int directly, hence
    the dispatch.

The fix also closes a quiet behavior gap that affected non-`#foreign`
globals (any `xx @<global>` from a helper fn). Surfaced while
investigating issue-0037; verified independently with a
non-`#foreign` sx-side global of type `s64`.

File mechanics: issue-0037 promoted to a focused feature example
per CLAUDE.md's resolution flow:
  examples/issue-0037.sx        -> examples/102-foreign-global-from-helper.sx
  tests/expected/issue-0037.{txt,exit} -> tests/expected/102-foreign-global-from-helper.{txt,exit}

ffi-objc-call-03 + ffi-objc-call-06 IR snapshots updated to
reflect the ptr→int store-via-ptrtoint shape that's now correct
at the LLVM-IR level (same bits in memory, but properly typed).
2026-05-19 19:18:31 +03:00
agra
e388687f1a ffi 1.8b: sret transform for #objc_call(>16 B non-HFA struct)
104/104 regression tests pass. The Triple round-trip
(triple_imp writes {11, 22, 33} on the IMP side → #objc_call(Triple)
reads them back) is the test of record.

emit_llvm.zig changes:

1. `objc_msg_send` arm — when `needsByval(ret_ty)` (same predicate
   the plain-foreign-call path uses), apply the sret transform:
     - ret type collapses to void
     - prepend a `ptr` param at index 0 (call site provides an
       alloca slot)
     - mirror `sret(<RetType>)` on the call site so the AArch64 x8
       / SysV-AMD64 hidden-ptr ABI lowers correctly
     - load the result from the slot post-call
   The IR shape now matches clang exactly:
     call void @objc_msgSend(ptr sret({...}) %slot, ptr %recv, ptr %sel)

2. `.ret` arm — the body-side counterpart for sx fns whose declared
   return type is sret-shaped (sx-defined IMPs registered via
   `class_addMethod` produce these). When the current function's
   `needsByval(func.ret)` predicate holds, store the IR ret value
   through the prepended sret slot (param 0) and emit `ret void`.
   Previously the unconditional coerceArg path turned the struct
   value into `undef` and emitted `ret void undef` — illegal LLVM.

Test mechanics: registers `SxTripleProbe : NSObject` at runtime via
`objc_allocateClassPair` + `class_addMethod`, IMP returns
Triple{11, 22, 33}. `#objc_call(Triple)(instance, "tripleValue")`
gets them back, round-trip pinned in the .txt snapshot and the
IR-shape snapshot.
2026-05-19 18:50:26 +03:00
agra
d43385112c ffi 1.6: objc_msg_send IR opcode + per-call-site LLVM fn type
102/102 regression tests pass; chess Android + iOS-sim still build
clean. `ffi-objc-call-04-primitive-returns` flips from xfail to
passing with both nil-recv and real-recv flavors of *void / s64
returns exercised.

Key change: a new `objc_msg_send` IR opcode bundles (recv, sel,
extra args) and carries the return type via the `Inst.ty` field.
emit_llvm.zig builds a per-call-site LLVM function type from the
argument Refs' IR types (recv/sel as ptr; extra args through
abiCoerceParamType) and dispatches with LLVMBuildCall2. One
declared `@objc_msgSend` symbol is reused across every return
type — opaque pointers make the function value type-erased, so
each call site picks its own ABI.

  before:  one (recv, sel) -> ptr LLVM declaration, hard-coded
           per call site; only void return wired in 1.3.
  after:   same declaration, each call site provides a fresh
           LLVMBuildCall2 fn-type → s64 / *void / bool / f64
           returns all dispatch correctly without separate FuncIds.

Selector init mechanism: stayed with the @llvm.global_ctors
constructor. Investigated clang's
`__DATA,__objc_selrefs` + `externally_initialized` shape — works
for fully-linked binaries (dyld substitutes the SEL at load
time) but **LLVM ORC JIT** (the engine behind `sx run`) doesn't
process Mach-O Obj-C metadata sections, so the slot keeps its
initial value (the method-name string pointer) and dispatch
crashes with "<null selector>". The portable choice: keep the
constructor AND inject a direct call to it at `main`'s entry —
idempotent under dyld (sel_registerName returns the same SEL on
re-registration), required for ORC JIT.

Files touched:
  src/ir/inst.zig    | new ObjcMsgSend struct + opcode
  src/ir/lower.zig   | drop the void-only restriction; emit the
                       new opcode; remove the orphaned
                       getObjcMsgSendFid path (objc_msgSend
                       declaration moved to emit_llvm)
  src/ir/emit_llvm.zig | objc_msg_send arm (per-call-site
                       LLVMBuildCall2); lazy `@objc_msgSend`
                       declaration via getObjcMsgSendValue;
                       emitObjcSelectorInit refactored to inject
                       the ctor call at main's entry
  src/ir/{print,interp}.zig | switch arms for the new opcode

`ffi-objc-call-03-selector-sharing.ir` snapshot updates to
reflect the new shape (the `call ... @objc_msgSend` call sites
no longer mention a typed wrapper).
2026-05-19 18:39:10 +03:00
agra
b8a412ddc7 ffi 1.5: intern Obj-C selectors — one static SEL slot per unique name
101/101 regression tests pass; the IR snapshot for the selector-
sharing test diff flips from four per-call `sel_registerName` calls
to two (one per unique selector) routed through a module-init
constructor — matching what clang emits for `@selector(...)`.

Hot-path cost collapses from a libobjc hashtable lookup per call to
a single load of a static `SEL*` slot:

  Before (Phase 1.3):
    %sel = call ptr @sel_registerName(<"init">)
    call ptr @objc_msgSend(<recv>, %sel)

  After (Phase 1.5):
    %sel = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init
    call ptr @objc_msgSend(<recv>, %sel)

  +  @OBJC_SELECTOR_REFERENCES_init    = internal global ptr null
  +  @OBJC_SELECTOR_REFERENCES_release = internal global ptr null
  +  define internal void @__sx_objc_selector_init() {
  +    %sel  = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_)
  +    store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_init
  +    %sel1 = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_.2)
  +    store ptr %sel1, ptr @OBJC_SELECTOR_REFERENCES_release
  +    ret void
  +  }
  +  @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }]
  +    [{ ..., ptr @__sx_objc_selector_init, ptr null }]

Implementation:
  module.zig    | new `objc_selector_cache: ArrayList(ObjcSelectorEntry)`
                  with `lookupObjcSelector` / `appendObjcSelector`. List
                  (not hashmap) keeps emit order stable across builds so
                  the IR snapshot doesn't flicker on rehash.
  lower.zig     | `internObjcSelector(sel)` creates the slot on first
                  use, returns the same `GlobalId` on every subsequent
                  call to the same selector. lowerFfiIntrinsicCall now
                  emits `global_addr + load` for literal selectors.
                  Non-literal selectors keep the `sel_registerName`
                  fallback. Declaring `sel_registerName` lazily on
                  first intern so emit_llvm finds it for the
                  constructor body.
  emit_llvm.zig | new `emitObjcSelectorInit` pass synthesizes a void
                  constructor that loops over the cache, calls
                  `sel_registerName` for each unique selector string,
                  stores the result in the slot. Constructor is
                  registered in `@llvm.global_ctors` with default
                  priority (65535) so dyld runs it before main.

The `@OBJC_METH_VAR_NAME_` private string globals and unnamed-addr
flag match clang's exact emission shape — picked up by the system
linker into the right Mach-O sections on macOS / iOS. Chess
Android + iOS-sim still build clean (no `#objc_call` in chess yet —
phase-3 migration will start exercising this).
2026-05-19 13:09:34 +03:00
agra
f43dea6913 ffi 1.3 make-green: #objc_call(void)(recv, "sel:") codegen
100/100 regression tests pass; ffi-objc-call-02-void-return flips
from xfail (codegen rejection) to passing ("ok").

Lowering for `#objc_call(void)(recv, "selector:")` lands in
lower.zig as `lowerFfiIntrinsicCall`:

  %sel  = call ptr @sel_registerName(<"selector:">)
  %call = call ptr @objc_msgSend(<recv>, %sel)

Two extern decls (`sel_registerName(*u8) -> *void` and
`objc_msgSend(*void, *void) -> *void`) are declared lazily and
cached on the Lowering instance via `objc_msg_send_fid` /
`sel_register_name_fid`, so multiple call sites share one
declaration each.

Phase 1.3 deliberately keeps scope tight: only `void` return + just
(recv, selector) arity is wired. Non-void returns + variadic arity
fall through with a diagnostic and are owned by subsequent phase-1
steps (1.6 primitive returns; 1.7..1.9 struct shapes; 1.10 multi-
keyword selectors).

Selector resolution is still per-call-site `sel_registerName` —
the planned 1.5 interning turns the per-call hashtable lookup into
a single static-global load. Chess Android + iOS-sim builds clean
— no regression on the existing typed-`objc_msgSend`-cast pattern.
2026-05-19 12:56:53 +03:00
agra
85bbb29e9e ffi 1.1: parser accepts #objc_call / #jni_call / #jni_static_call
98/98 regression tests pass; ffi-objc-call-01-parse flips from
parse-error xfail to passing.

Shape: `#<intrinsic>(ReturnT)(args...)`. The return-type generic
sits in the first parens, the actual call args in the second. All
three intrinsics share the same parse rule; only the kind tag and
the downstream lowering differ.

  token.zig    | three new hash_* tags
  lexer.zig    | matches the directive keywords with the same
                 isIdentContinue boundary check as the rest
  ast.zig      | FfiIntrinsicCall node with `kind`, `return_type`,
                 and `args` fields; FfiIntrinsicKind enum
  parser.zig   | parseFfiIntrinsicCall — same call-arg loop shape
                 as Call, with the leading return-type slot
  sema.zig     | analyzeNode + findNodeAtOffset arms walk the args
                 + return-type child nodes
  lsp/server.zig | classify the new tokens as ST.keyword

Codegen for the new intrinsic isn't wired yet — examples that
reach the body of a non-suppressed call would fail at lowering.
The current parse test uses `inline if false { ... }` to suppress
the dead branch, so sema/codegen don't see the node. Phase 1.3+
adds the lowering and the gate comes off.

Chess Android + iOS-sim builds clean — no regression on the
existing `objc_msgSend` cast pattern or the JNI helper.
2026-05-19 12:45:49 +03:00
agra
43a30e727d imports: read resolved C-import paths after mutation, not before
Latent bug from the stdlib-path resolution introduced in 4849cfb.
The earlier shape captured `const ci = decl.data.c_import_decl;`
BEFORE mutating `decl.data.c_import_decl.{sources,includes}` with
the resolved paths, then passed the stale `ci.includes` to
`c_import.processCImport`. Result: `#include "vendors/..."` paths
that resolved via the stdlib branch (i.e. only existed under
sx/library/vendors/) reached clang as the original unresolved
string and failed to parse — silently producing no synthesized
`#foreign` decls.

`#source` survived because the source list is re-read from
decl.data later (collectCImportSources walks the AST), so it
picked up the mutated value. Only `#include`'s synthesis path was
broken.

Fix: do the resolution first inside its own scope, then re-bind
`ci` from `decl.data.c_import_decl` so the include list passed to
processCImport sees the resolved paths.

Caught by ffi-07 baseline (next commit) — the test deliberately
puts its C helper only under library/vendors/ so the path is
findable solely via the stdlib chain.
2026-05-19 11:51:20 +03:00
agra
7fd6decdc9 emit_llvm: sret return for >16-byte aggregate foreign returns
Foreign functions that return a >16-byte non-HFA aggregate (e.g.
Big24 / UIEdgeInsets on iOS / clang-shaped struct returns) need the
indirect-return ABI: caller allocates space, passes its pointer as a
hidden first arg with `sret(<T>)`, callee writes through it and
returns void. AAPCS64 puts the pointer in x8; SysV AMD64 puts it in
the first int register and treats the named return as void.

The existing >16-byte branch in `abiCoerceParamType` was returning
`ptr` for BOTH params and returns. That works for byval params (the
established pattern — caller alloca + store + pass ptr, callee loads
in prologue), but is wrong for returns: it caused the function decl
to look like `ptr @fn(...)` rather than `void @fn(ptr sret(<T>), ...)`,
and the call site read whatever happened to be in x0 as a struct
pointer — segfault on dereference (caught while writing the ffi-03
baseline).

Fix layered into the same `abiCoerceParamType` / call-site code path:

  emitFunctionDecl:
    - Compute `uses_sret = needs_c_abi && needsByval(ret_ty, raw_ret_ty)`.
    - Ret type collapses to void.
    - Prepend a `ptr` param at slot 0.
    - Add `sret(<RetType>)` type attribute on param-index 1
      (LLVMAttributeIndex 1 = first parameter; 0 = return value).

  .call lowering:
    - Detect callee_uses_sret via the same predicate.
    - Allocate the result on the caller's stack (`sret.slot`).
    - Prepend it as args[0] (with sret_off index alignment so the
      original sx args land at args[1..]).
    - After LLVMBuildCall2, set the same `sret(<T>)` attribute on
      the call site's arg 1 (mirrors the fn-decl attribute — both
      land in the AArch64 backend's lowering pass).
    - Load the result from the slot to produce the IR value.

`call_indirect` (function-pointer dispatch — uikit.sx's typed
`objc_msgSend` casts) keeps its existing behavior for now; the iOS
path already round-trips UIEdgeInsets via that route. Folding the
same sret transform into call_indirect is a follow-up.

89/89 regression tests still pass. Chess Android + iOS-sim both
build clean.
2026-05-19 11:40:54 +03:00
agra
7d2c2fb062 emit_llvm: bridge struct<->array ABI for 9..16-byte foreign structs
Resolves issue-0036 (LLVM verifier failure on 16-byte integer-only
struct by value through #foreign). The mismatch:

  Call parameter type does not match function signature!
    %load = load { i64, i64 }, ptr %alloca, align 8
  [2 x i64]  %call = call [2 x i64] @fn({ i64, i64 } %load)

`abiCoerceParamType` had already chosen `[2 x i64]` for 9..16-byte
non-HFA structs (the AAPCS64 / SysV AMD64 register-pair ABI slot for
that size class) on the foreign-decl side, but `coerceArg` only knew
how to bridge struct<->integer (the ≤8 B case) — not struct<->array.
LLVM's verifier rejects type-mismatched call args, so the call site
never landed.

Added the symmetric branches in coerceArg:
  - Struct -> Array : alloca <array>; store <struct>; load <array>
  - Array -> Struct : alloca <array>; store <array>;  load <struct>

Both use the LLVM opaque-pointer memory-bitcast pattern already in
place for the integer case. They're paired with the existing
i64 <-> small-struct bridge so all four (≤8 B int, 9..16 B int,
16 B HFA, >16 B byval) ABI slots round-trip cleanly through
emit_llvm now.

File mechanics: promotes the issue-0036 repro to a focused feature
example per CLAUDE.md's issue-resolution workflow:

  examples/issue-0036.sx              -> examples/101-ffi-medium-struct.sx
  tests/expected/issue-0036.{txt,exit} -> tests/expected/101-ffi-medium-struct.{txt,exit}
  vendors/issue_0036/issue_0036.c     -> vendors/ffi_medium_struct/ffi_medium_struct.c

Snapshot updated to the passing output. 89/89 regression tests pass;
chess Android build still clean.
2026-05-19 11:31:04 +03:00
agra
4849cfb904 android: dpi_scale, scissor, JNI safe-insets, touch input
Four Android UX wins landing together; all verified end-to-end on a
Pixel 7 Pro (board fills width, info-panel text renders, status bar
inset honored, tap-to-select + tap-to-move plays 1. e4).

- AndroidPlatform.init reads density via AConfiguration_getDensity
  (app->config at offset 32) and sets dpi_scale = density / 160. The
  hardcoded 1.0 had been making every logical unit equal one physical
  pixel; ChessBoardView's 520-default size_that_fits fallback then
  rendered at ~half the framebuffer width on the device, and glyphs
  rasterized at literal 11-13 physical pixels were essentially invisible
  on a 2340-tall display.
- gles3.sx set_scissor un-stubbed; with dpi_scale right the renderer
  feeds in valid pixel bounds and the Y-flip math lands inside the
  framebuffer.
- New library/vendors/sx_android_jni/sx_android_jni.c walks
  activity -> window -> decorView -> rootWindowInsets via JNI and
  publishes the system-bar insets. safe_insets() lazy-queries the
  first call after EGL is up (decor view isn't attached at bootstrap).
- sx_android_install_input_handler sets app->onInputEvent; sx-side
  sx_android_input_event translates AMotionEvent DOWN/MOVE/UP/CANCEL
  into existing mouse_down/mouse_moved/mouse_up Events so the chess
  board's tap-to-select + ScrollView drag path Just Works. Coordinates
  divided by dpi_scale so layout-side hit tests match. poll_events
  drains its slice after returning (mirrors the SDL pattern).
- src/imports.zig now routes #import c { #source / #include } paths
  through the same chain as #import (importing dir -> CWD -> stdlib
  search paths). Lets library-owned C helpers like the JNI bridge
  live in sx/library/vendors/ without forcing consumers to vendor a
  copy. Existing CWD-relative consumer layouts (chess's vendors/...)
  still resolve first, so no regression.

86/86 regression tests pass.
2026-05-19 11:09:41 +03:00
agra
b5bf789b7b android: AAssetManager bootstrap + APK asset bundling + scissor TODO
platform/android.sx: `sx_android_bootstrap(app)` now also reads the
ANativeActivity's `assetManager` (offset 64) and `internalDataPath`
(offset 32) into module globals so consumers can route file I/O
through the APK's bundled `assets/` tree.

target.zig (`createApk`): also zips the project's `./assets/`
directory into the APK alongside `lib/<arch>/`. Resolves relative
to the user's CWD at invoke time — matches the convention chess
uses (assets/ next to main.sx).

gles3.sx: scissor is currently a no-op on Android. The renderer's
ScrollView clip_push path feeds bounds that land outside the
framebuffer (clipping everything off-screen). With scissor disabled
the chess board + pieces render correctly. TODO recorded in the
file to fix the bounds path properly.
2026-05-19 10:09:30 +03:00
agra
d8968ae093 android: forward NDK sysroot to embedded clang + skip auto #library/#framework
C imports (stb_image, stb_truetype) compiled via the embedded LLVM
clang library now resolve bionic headers on Android. main.zig auto-
fills target_config.sysroot with the NDK root (mirroring the iOS path
that auto-fills the iOS SDK path); c_import.zig derives the bionic
sysroot inside it and passes `--sysroot <ndk>/toolchains/llvm/prebuilt/<host>/sysroot`
to the embedded clang.

Android link branch in target.zig stops auto-appending entries from the
collected `#library` / `#framework` lists. Most `#library` directives
in the stdlib (`objc.sx`'s `objc :: #library "objc";`, frameworks set
by uikit.sx, etc.) describe Apple-specific intent that's nonsensical on
Android. Users opt into Android-side libs via `opts.add_link_flag(...)`
in build.sx — same shape as how the iOS branch already lists frameworks
in chess's configure_build.

Verified end-to-end: chess game compiles for Android, packages into a
debug-signed APK, installs and launches on Pixel 7 Pro. It crashes in
UIRenderer.init because raw GL calls run before EGL is up — same
architectural gap iOS bridges via the Metal GPU protocol. Next step
is a GLES3 GPU impl or a lazy-init in UIRenderer. 86/86 regression
tests + iOS-sim chess build clean.
2026-05-19 00:36:05 +03:00
agra
561ad03a7c android: Platform-owned entry bridge + .android OS enum variant
User writes BOTH `main` and a 3-line `android_main(app)` trampoline.
The library provides `sx_android_bootstrap(app)` (stashes the NDK app
pointer into a platform-owned global) and `AndroidPlatform` impl of
the Platform protocol. The library NEVER references `main` — the OS-
shape entry symbol lives in user code where the other entry symbols
already live. iOS / SDL3 keep their existing shape; only Android adds
the trampoline.

Cross-cutting bits this commit ships:

  library/modules/compiler.sx
    Add `android` variant to `OperatingSystem`.

  src/ir/lower.zig
    - injectComptimeConstants: map TargetConfig.isAndroid() → .android.
    - New Pass 4 `checkRequiredEntryPoints`: emit a clean diagnostic
      when `--target android` is requested but `android_main` isn't
      defined, instead of letting the user crash on a dlopen-time
      missing-symbol error.

  library/modules/platform/android.sx
    AndroidPlatform impl of the Platform protocol — EGL bringup on
    `APP_CMD_INIT_WINDOW`, ALooper(0) polling, dispatches the user's
    frame closure each ~16 ms tick. `sx_android_bootstrap(app)` is the
    only function exposed for the entry trampoline.

  examples/99-android-egl-clear.sx
    Rewritten to use the new pattern: minimum `main` + `android_main`
    pair, AndroidPlatform-driven render loop. Doubles as the usage
    reference users hand off to the compiler diagnostic.

Verified on Pixel 7 Pro: purple clear-color frame, periodic
`rendered 60 frames` logcat lines. iOS-sim chess + 86/86 regression
tests pass.
2026-05-19 00:23:33 +03:00
agra
efb087559d ir: auto-deref *Self when invoking a Closure-typed field (issue-0035)
When lowering `self.cb()` from inside a method whose receiver is *Self,
the field-access path passed the receiver pointer (not the aggregate)
to `structGet`, which then produced `call void undef(ptr undef)` at
the LLVM level — undefined at runtime, corrupted adjacent globals when
it transferred control to a garbage pointer. Auto-load through the
pointer first so structGet receives a real aggregate.

Discovered while building the new AndroidPlatform's `run_frame_loop` —
calling the stored frame closure as `self.frame_closure()` zeroed
out adjacent globals because the undef call jumped into random memory.

Added examples/100-closure-field-call-via-self-ptr.sx as the locked-in
regression: both direct (`self.cb()`) and hoisted (`fn := self.cb; fn();`)
forms must yield identical IR + behavior. 86/86 regression tests pass.
2026-05-19 00:22:35 +03:00
agra
f66cda6d11 android target + APK pipeline; LSP imports honor stdlib paths
Android (toolchain):
  --target android / --target android-arm64 → aarch64-linux-android21.
  target.zig discovers $ANDROID_NDK_HOME (or scans
  ~/Library/Android/sdk/ndk/* for the newest), invokes the NDK clang
  with -shared -fPIC and links libsxhello.so against -llog -landroid
  -lEGL -lGLESv3 -lm -ldl. native_app_glue.c from the NDK is compiled
  and linked alongside the sx .o so apps can use the conventional
  android_main(struct android_app*) shape; -u ANativeActivity_onCreate
  keeps glue's symbol live.

Android (APK):
  --apk <out> wraps the .so into a debug-signed installable APK.
  target.zig discovers the SDK at $ANDROID_HOME (or
  ~/Library/Android/sdk), picks the newest build-tools + platforms,
  generates a NativeActivity AndroidManifest.xml from --bundle-id,
  packages via aapt2 link, appends the lib/ tree, zipalign, then
  apksigner against ~/.android/debug.keystore (auto-generated via
  keytool on first use). One command end-to-end:
      sx build --target android --apk out.apk \\
          --bundle-id co.swipelab.foo main.sx
  Verified on Pixel 7 Pro: install + launch reaches android_main.

Compiler (entry-point linkage):
  Top-level fn defs default to LLVM internal linkage and are lazily
  lowered (only `main` was eagerly lowered before). Added
  isExportedEntryName() — a small allowlist for names the OS loader
  calls: `main`, `android_main`, `ANativeActivity_onCreate`,
  `JNI_OnLoad`. These get eagerly lowered AND keep external linkage,
  so they actually land in .dynsym.

LSP (imports):
  DocumentStore now takes the install-discovered stdlib_paths and
  forwards them into resolveImportPath, mirroring the compiler. Before
  this, every `#import "modules/..."` resolved through the stdlib path
  failed silently inside the LSP and identifiers from those modules
  showed as `undefined variable`. Repro on label.sx: 1 false positive
  before, 0 after.
2026-05-18 23:09:55 +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
3622993311 ui: chess UI renders on iOS sim via Metal (scene lifecycle + alias fix)
Four root causes for "chess UI shows white screen" — all fixed:

1. Hybrid legacy-app + scene-API path on iOS 26. Without
   UIApplicationSceneManifest in the Info.plist, iOS 26 booted us in
   [rb-legacy] mode and -[UIApplication connectedScenes] returned an
   empty set. didFinishLaunching's window-setup code bailed at "no scene"
   and the UIWindow never appeared on screen. Fix: emit the manifest in
   buildInfoPlist (src/target.zig) AND split the window/view/layer setup
   from didFinishLaunching into a new SxSceneDelegate's
   scene:willConnectToSession:options: IMP. didFinishLaunching now just
   subscribes the keyboard observer and returns YES.

2. UISceneDelegate formal protocol conformance. iOS 26 checks
   -[cls conformsToProtocol:@protocol(UISceneDelegate)] before
   instantiating the scene delegate; without it the runtime logs
   "SxSceneDelegate does not conform to the UISceneDelegate protocol"
   and silently uses a default delegate that does nothing. Fix:
   look up UISceneDelegate + UIWindowSceneDelegate via objc_getProtocol
   and class_addProtocol BEFORE objc_registerClassPair. The protocol
   metadata is present at link time (unlike UIApplicationDelegate per
   the long-standing legacy note in CHECKPOINT).

3. Protocol method return types via type aliases lowered as void.
   The GPU protocol declares `create_shader(...) -> ShaderHandle` where
   `ShaderHandle :: u32`. The protocol-decl lowering at lower.zig:7547
   passed the return AST node through type_bridge.resolveAstType which
   doesn't know about the type_alias_map. resolveTypeName fell through
   to its "assume named struct" branch and registered ShaderHandle as
   an empty struct ({ }). LLVM IR for the protocol call_indirect then
   read `call {} %fn_ptr(...)` — return value discarded; the
   subsequent abi.coerce load from a zero-init'd alloca yielded 0.
   Symptom: UIRenderer.mtl_shader = 0, set_shader sees state == null,
   the render-encoder fires draw with no pipeline state bound, GPU
   rejects the command buffer with MTLCommandBufferErrorInternal.
   Fix: at the protocol-decl method-type resolution sites in
   lower.zig, check type_alias_map BEFORE falling through to
   type_bridge.resolveAstType for both params and return type. A
   chess-side companion fix in /Users/agra/projects/game/main.sx
   (separate commit) memsets the MetalGPU struct after alloc so the
   List(*void) fields' len/cap/items aren't garbage.

After all four (this commit + memset companion in chess repo):
- 71/71 regression tests pass.
- Chess game now boots, scene-connects, ticks CADisplayLink, renders
  dark-gray clear + UI text + panel dividers every frame on iOS sim.
- Metal-clear example still renders.

Chess board + pieces visual contrast and faint-text-color are remaining
visual-polish items, not compiler/platform-setup issues.
2026-05-18 08:42:22 +03:00
agra
63565e41ff abi: pass >16B aggregates by ptr-in-next-reg (Apple ARM64 ABI) + Path B for fn-ptr casts
Three stacked compiler bugs were causing iOS-sim chess to crash inside
[MTLTexture replaceRegion:...]. Fixing them lets every replaceRegion call
site succeed (1×1 RGBA8, 1MB R8 atlas, 440×440 chess pieces).

Path B for callconv(.c) fn-pointer casts:
- FunctionInfo now carries call_conv: CallConv (TypeInfo.CallConv) so
  function-type interning distinguishes sx-CC from C-CC. Inst.zig's
  Function.CallingConvention aliases the same enum.
- Parser accepts an optional `callconv(.c)` suffix on fn-pointer type
  spellings (factored into parseOptionalCallConv() shared with parseFnDecl
  and parseLambda).
- resolveFunctionType passes the parsed CC through functionTypeCC().
- .call_indirect reads fp.call_conv == .c and applies the C-ABI
  alloca+materialize for >16B aggregate args (Path A's behaviour at .call).

Apple ARM64 ABI (drop LLVM byval):
- Side-by-side asm diff vs clang's emission for the equivalent C call site
  showed LLVM's `byval` attribute lowers Apple-arm64 byval on the stack,
  while clang passes the struct via a pointer in the next int register
  (x2 for replaceRegion:). The runtime objc_msgSend dispatch path expects
  clang's convention.
- Dropped the byval attribute from the function-signature emission and
  from both call sites (.call and .call_indirect). The materialize-into-
  alloca + pass-plain-ptr pattern stays — the call site now matches
  clang's `mov x2, sp` exactly.
- Path A's sx-to-sx case continues to work since both ends use plain ptr
  (caller does alloca+store+pass, callee loads from the ptr in prologue).

Protocol dispatch (emitProtocolDispatch):
- Untargeted `null` lowers as const_null with type .void (per
  target_type orelse .void). The "wrap-value-in-alloca-pass-pointer"
  branch alloca'd a void slot, which LLVM's IRBuilder asserts on —
  EXC_BREAKPOINT in getTypeSizeInBits, manifesting as exit 133 / SIGTRAP
  when building the chess game. Fixed by re-emitting as
  constNull(void_ptr) when arg_ty == .void && expected_ty == void_ptr.
- is_pointer_ty only recognized .pointer, so [*]T (many_pointer) was
  alloca-wrapped — the heap pixels pointer from stbi_load was stored
  into a stack slot and the slot's address was passed as the *void arg.
  Fixed by extending the check to `.pointer or .many_pointer`.

metal.sx call sites + lifecycle guards:
- msg_replace (replaceRegion:, MTLRegion = 48B) and the two setScissorRect:
  sites (MTLScissorRect = 32B) now spell their fn-pointer types with
  by-value params + callconv(.c) — the *MTLRegion/@local workaround is
  gone.
- metal_begin_frame_ios bails before nextDrawable when pixel_w/h are 0
  (drawableSize 0×0 makes nextDrawable abort via XPC).
- metal_init_ios only sets drawableSize when dims are positive.
- begin_frame's encoder/cmd_buffer failure paths now clear self.drawable
  so a partial failure doesn't leak a drawable back into the pool.

Examples + tests:
- examples/86-callconv-c-fnptr-large-aggregate.sx — new, covers Path B
  with C-CC fn-ptr cast.
- examples/87-fnptr-cast-large-aggregate.sx — renamed from issue-0025.sx,
  covers Path B with default sx-CC (the negative case).
- examples/85-cc-c-large-aggregate.sx — from Session 60, covers Path A.
- examples/issue-0014.sx, issue-0024.sx, issue-0025.sx — removed
  (resolved earlier this work).

71 regression tests pass, 0 failed. Chess game builds clean for iOS sim
and reaches its frame loop without aborting. Runtime: chess UI still
doesn't render — remaining issue is in the UIKit lifecycle / CAMetalLayer
setup (legacy-app vs scene-API hybrid), not a compiler bug. See
current/CHECKPOINT.md "Next step" for the diagnosis + options.
2026-05-18 00:11:23 +03:00
agra
a938c4f900 metal: GPU protocol + MetalGPU renders MSL triangle on iOS
Phase 8 step 3a of the Metal renderer port:

- New library/modules/gpu/ with types.sx (handles + ClearColor +
  TextureFormat enum), api.sx (GPU :: protocol { ... } covering the
  lifecycle / per-frame / resource / per-draw surface), and metal.sx
  (MetalGPU backend implementing the protocol against CAMetalLayer).
  Resource handles are 1-based indices into backend List(*void) tables.
  MTL aggregates >16 bytes (MTLRegion, MTLScissorRect) pass via *T to
  match arm64 Apple's indirect-by-reference ABI; MTLClearColor + CGSize
  go through the HFA path as direct fn-pointer casts on objc_msgSend.

- UIKitPlatform got a gpu_mode: GpuMode toggle + sibling SxMetalView
  class registration. In metal mode init skips EAGL context, the
  did_finish_launching IMP skips the EAGL drawable-properties dict,
  layoutSubviews reads the layer's bounds * dpi_scale into pixel_w/h
  instead of allocating a GL renderbuffer, and end_frame is a no-op
  (the MetalGPU owns its own present).

- examples/63-metal-clear.sx verifies the pipeline end-to-end on iOS
  sim — compiles a pass-through MSL shader (packed_float2/packed_float4
  to avoid alignment padding), uploads 3 vertices, draws a colored
  triangle on a dark-blue clear.

Compiler fixes (filed-and-fixed in this branch):

- inline if X { return E; } followed by a fall-through final expression
  no longer emits two terminators into the same basic block. Verified
  by examples/83-inline-if-return-fallthrough.sx.

- Top-level type alias Name :: u32; now resolves correctly as the type
  annotation on a global variable (was treated as ptr {}, breaking
  comparisons + initializers). Verified by examples/84-global-type-alias.sx.

Issue->feature promotion:

- 16 historical examples/issue-NNNN.sx repros now confirmed-fixed and
  renamed to focused feature names (67-82). Each gains a
  tests/expected/*.txt + .exit pair so the regression suite covers them.

- 5 stale issue repros deleted (subsumed by broader tests).

Regression suite: 68 passing, 0 failed. macOS chess builds + runs; wasm
chess builds; iOS sim GLES chess still renders the full board; iOS sim
Metal demo renders the triangle.
2026-05-17 19:36:37 +03:00
agra
e8bd40f710 ios: framework embedding in .app + -F search paths
- `-F <dir>` CLI flag adds Apple framework search paths (parallel to `-L`).
- `TargetConfig.framework_paths` flows into the iOS link line (`-F<dir>`).
- iOS link adds `-Wl,-rpath,@executable_path/Frameworks` so embedded
  frameworks resolve at runtime.
- `createBundle` now takes the framework list; for each one it locates
  `<name>.framework` in the `-F` paths and `cp -R`s it into
  `<bundle>.app/Frameworks/`.
- `c_import.compileCToObjects` forwards `-target`/`-isysroot` to clang so
  `#c_import` works under cross-compile (was using host clang implicitly).
- iOS SDK is auto-discovered once at startup and shared by both the C
  compile and the link paths.
- `SX_DEBUG_LINK=1` prints the resolved link argv.
- `library/modules/sdl3.sx`: drop `#library "SDL3"` — linking is now
  per-target (build.sx handles `-lSDL3` on macOS, `-framework SDL3` on iOS).
2026-05-17 14:38:58 +03:00
agra
c027e1969b stdlib: relocate modules under library/
- examples/modules/ -> library/modules/ (top-level, no more
  symlink hacks in consumer projects)
- compiler discovers stdlib via _NSGetExecutablePath / readlink
  /proc/self/exe; searches dev layout (../../library), install
  layout (../library), and alongside-binary fallback
- SX_STDLIB_PATH env var overrides for tests / dev convenience
- SX_DEBUG_STDLIB env var dumps the discovery results
- build.zig installs library/ alongside the binary
- Compilation gains stdlib_paths field threaded through resolveImports
- 50 tests pass; consumer projects can now build from any cwd
2026-05-17 13:49:25 +03:00
agra
535e7b9c24 zig 0.16.0 stable
std.time.Instant -> std.Io.Timestamp (Timing helper gets an io handle).
.monotonic clock variant -> .awake.
50 tests pass.
2026-05-17 13:26:07 +03:00
agra
1c32d54e01 ios + ir cleanup
- ios: --target ios/ios-sim shorthands, iOS SDK auto-discovery,
  #framework directive + BuildOptions.add_framework hook,
  .app bundle + Info.plist + codesign (ad-hoc and real),
  --codesign-identity/--provisioning-profile/--entitlements flags,
  modules/std/{objc,uikit}.sx, dynamic class registration,
  typed objc_msgSend cast pattern, UIApplicationMain handoff,
  UIWindow scene attach. Runs on iPhone hardware.
- ir: silent .s64 defaults → loud diagnostics,
  resolveReturnType infers from body, sub-byte int sizes match LLVM,
  tuple type interning includes names, compile errors exit 1
- issue-NNNN convention: resolved bugs rename to focused features
- 50 regression tests passing
2026-05-17 13:19:08 +03:00
agra
69934592d8 c import 2026-03-06 10:46:28 +02:00
agra
f9dda972d2 fixes 2026-03-05 16:20:36 +02:00
agra
22bc2439ce fixes 2026-03-04 17:12:56 +02:00
agra
67e02a20a5 ... 2026-03-04 09:18:24 +02:00
agra
0336f361c7 issue 06 2026-03-03 16:18:58 +02:00
agra
004aff5f67 wasm shell + destructuring 2026-03-03 13:21:54 +02:00
agra
6c5672c7df trailing commas 2026-03-03 09:42:01 +02:00
agra
03074472e5 build options #compiler 2026-03-03 09:35:50 +02:00
agra
bbb5426777 sm 2026-03-02 21:00:55 +02:00
agra
2f4f898d54 asm... 2026-03-02 17:19:41 +02:00
agra
ba9c4d69ce wasm 2026-03-02 09:49:43 +02:00
agra
f763765ea2 ir done'ish 2026-03-01 22:38:41 +02:00
agra
6a920dbd2c ir 2026-02-28 18:03:38 +02:00
agra
2552882ce6 05 2026-02-26 14:46:21 +02:00
agra
dd14f1206b ir 2026-02-26 02:25:02 +02:00
agra
7209e8e69d ... 2026-02-26 00:18:36 +02:00