Commit Graph

51 Commits

Author SHA1 Message Date
agra
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.
2026-05-25 09:50:37 +03:00
agra
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.
2026-05-25 09:35:15 +03:00
agra
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.
2026-05-25 09:13:36 +03:00
agra
4bf5908792 mem: Steps 5-7 — context-identifier rebind + interp ctx bootstrap
Step 5 — `context` resolves through `current_ctx_ref`. The compile-time
emit of the default GPA into the `context` global is gone; entry points
already bind `current_ctx_ref` to `&__sx_default_context` and every
sx-to-sx call forwards it. `allocViaContext` sources from
`current_ctx_ref` too. `matchContextAllocCall` is kept as a comptime
escape hatch: the ct_module spun up by `evalComptimeString` doesn't get
the full Allocator/CAllocator/Context type registration so the protocol-
dispatch chain wouldn't run in the interp; codegen also wins from the
direct libc malloc/free.

Step 6 — `push Context.{...}` stack-discipline rewrite. Allocates a
fresh `Context` slot, binds `current_ctx_ref` to it for the body's
lexical scope, restores on exit. No global, no walk.

Step 7 — interp parity. `defaultContextValue()` builds the Context
aggregate (CAllocator thunks for alloc/dealloc, null data) on demand.
`interp.call` bootstraps slot_ptr(0) when an entry function with
implicit ctx is called sans args; `materializeCtxArg` dereferences the
caller's slot_ptr into the aggregate at every sx-to-sx call boundary so
the callee's `load(ref_0)` lands on the value; `load` of an aggregate
is a passthrough. `.global_addr` of `__sx_default_context` returns the
aggregate directly so exported entries' first-line `global_addr(...)`
runs cleanly in `#run`.

`ct_lowering` inherits `implicit_ctx_enabled` + `has_implicit_ctx` so
functions lowered into the ct module carry ctx like their main-module
twins.

152/152 example tests pass. Snapshots regen.
2026-05-25 09:10:04 +03:00
agra
92c6b47f12 mem: Step 3 — thread __sx_ctx through closure/fn-pointer/method dispatch
Continues the implicit-Context refactor. Bare-fn trampolines, lambda
trampolines, and protocol thunks now carry __sx_ctx at slot 0; call
sites for closures, fn-pointer variables, and method dispatch prepend
the caller's current ctx.

- emit_llvm.zig:1687 call_indirect treats `fp_ctx_slots` leading args
  as opaque ptr (the implicit ctx) when the fn-pointer is default-conv
  under has_implicit_ctx.
- lower.zig:fnPtrTypeWantsCtx predicate gates the prepend at both
  scope-local and global fn-pointer call sites.
- lower.zig:fixupMethodReceiver skips __sx_ctx when probing the
  receiver param's type.
- lower.zig:lowerLambda builds closure type from user-visible params
  only (skip ctx + env).
- lower.zig:closure(bare_fn) builds closure type from user-visible
  params only.
- module.zig: Module.has_implicit_ctx flag mirrors Lowering's switch
  so emit_llvm can read it without a back-pointer.

Tests updated:
- 5 ObjC-block/runtime tests get `callconv(.c)` on fn-ptr types
  cast from `objc_msgSend` / Block.invoke (C-side calls into sx).
- ffi-06-callback gets `callconv(.c)` on double_it/add_with_ctx —
  the registered C-side callbacks.
- 08-types snapshot regen (undefined-init drift from layout shift).
- 11 JNI/ObjC .ir snapshots regen for the ctx-prepended thunk
  signatures.

151/152 example tests pass. Remaining failure (05-run) is the
comptime/interp path that requires Step 7 (callWithDefaultContext).
2026-05-25 08:41:50 +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
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.
2026-05-23 01:28:32 +03:00
agra
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.
2026-05-22 19:03:31 +03:00
agra
30fed66616 ffi #foreign: C-variadic tail via args: ..T
Trailing `args: ..T` on a #foreign declaration now lowers to the C
calling convention's `...` instead of sx-side slice-packing. Drops
the per-arity #foreign-shim workaround for callers of variadic C
APIs (__android_log_print, printf-family, etc.). Closes issue-0043.

- IR: Function.is_variadic on inst.Function; declareFunction drops
  the variadic param from the IR signature for foreign+variadic
  decls.
- emit_llvm: LLVMFunctionType receives is_var_arg=1 when the flag
  is set; call lowering passes extras through unchanged.
- Lowering: packVariadicCallArgs early-outs for foreign+variadic
  (no slice-pack); new promoteCVariadicArgs applies C default
  argument promotion (bool/s8/s16/u8/u16 -> s32, f32 -> f64) to
  extras past the fixed param count.
- Test: examples/ffi-foreign-cvariadic.sx + .c exercise s64/f64/s32
  returns through C va_arg over s32/f64/*u8 element types.

134 host + 6 cross tests pass on the WIP-less baseline.
2026-05-22 13:13:43 +03:00
agra
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.
2026-05-20 19:50:25 +03:00
agra
c02b6b3b1b ffi #jni_main: Alias.new(args) constructor dispatch via JNI NewObject
Adds the constructor-invocation arm of the foreign-class DSL:
`SurfaceView.new(ctx)` (where `SurfaceView` is a `#foreign #jni_class`
with `static new :: (ctx: *Context) -> *Self;`) lowers to
`FindClass(env, "android/view/SurfaceView") + GetMethodID(env, cls,
"<init>", "(args)V") + NewObject(env, cls, mid, args...)`. Returns
the fresh jobject.

  - inst.zig: `JniMsgSend.is_constructor` flag + `parent_class_path`
    re-purposed to carry the class being constructed (alongside its
    existing nonvirtual-super-class use). Mutually exclusive with
    `is_static` / `is_nonvirtual`.
  - lower.zig: `lowerCall.field_access` arm now recognises
    `Alias.method(args)` where `Alias` resolves in `foreign_class_map`
    and the matching member is `static`. `new` routes to a new
    `lowerForeignStaticCall` that derives a `(args)V` JNI descriptor
    and emits a `JniMsgSend` with `is_constructor=true`. Non-`new`
    static calls report a clear "use #jni_static_call" diagnostic
    until that sugar lands.
  - emit_llvm.zig: new `NewObject` vtable slot (28) + `emitJniConstructor`
    helper expanding the FindClass+GetMethodID+NewObject chain. The
    jni_msg_send arm short-circuits to it when `is_constructor` is set.

Smoke `ffi-jni-main-03-ctor.sx` exercises both this slice and the
previous super-dispatch slice in a single `onCreate` body: calls
`super.onCreate(b)` then constructs a `SurfaceView` with the Activity
as Context. IR shows the expected six-stage chain (FindClass+GetMethodID+
CallNonvirtual + FindClass+GetMethodID+NewObject); APK builds clean.

Naming caveat: the Java type `android.content.Context` clashes with
sx stdlib's `Context :: struct {...}` (heap-context). The smoke aliases
it `JContext` — future work could add a path-prefix or `as` rename
form on `#jni_class` to avoid the manual rename.

133 host / 6 cross / zig build test all green.
2026-05-20 17:14:51 +03:00
agra
d946e3d577 ffi #jni_main: sx-side super.method(args) dispatch via CallNonvirtual<T>Method
Inside a `#jni_main` (or any sx-defined `#jni_class`) bodied method,
`super.method(args)` lowers to JNI's nonvirtual dispatch against the
parent class resolved via `#extends` (default `android.app.Activity`).

  - lower.zig: tracks `current_foreign_class` + `current_foreign_method`
    around each `synthesizeJniMainStub` body; pushes the JNIEnv* arg
    onto the lexical `#jni_env` stack so omitted-env JNI calls inside
    the body see env without a wrapper. New `lowerSuperCall` handles
    the `super.method(args)` receiver pattern: derives parent path,
    reuses the enclosing method's signature when names match (the
    common `super.<override>(args)` case), or looks up the method on
    the parent class declared as `#foreign #jni_class`.
  - inst.zig: `JniMsgSend` gains `is_nonvirtual: bool` and
    `parent_class_path: ?[]const u8` — the dispatch tag + super class
    foreign path. Mutually exclusive with `is_static`.
  - emit_llvm.zig: new `CallNonvirtual<T>Method` vtable slots + a
    fourth dispatch arm. Resolves the parent jclass via
    `FindClass(env, parent_path)` (per-call; caching is follow-up),
    then `GetMethodID(env, parent_cls, name, sig)`, then
    `CallNonvirtual<T>Method(env, obj, parent_cls, mid, args...)`.

Disassembly on the smoke confirms the chain:
`ldr [env+0x30]` (FindClass) → `ldr [env+0x108]` (GetMethodID) →
`ldr [env+0x2d8]` (CallNonvirtualVoidMethod) with `(env, self,
parent_cls, mid, bundle)`.

132 host / 5 cross / zig build test all green. The slice unblocks
Activity lifecycle overrides (onCreate, onResume, onPause) calling
their required `super.<method>(args)` without raw `#jni_call`
boilerplate.
2026-05-20 16:57:30 +03:00
agra
3300bfb0df ffi #jni_main R.4: require #jni_main on Android; retire android_main acceptance
`checkRequiredEntryPoints` no longer accepts `android_main` as an
Android entry — `#jni_main #jni_class("...")` is now the sole accepted
path. The diagnostic walks the user through declaring an Activity +
Bundle foreign decl. `isExportedEntryName` drops `android_main` and
`ANativeActivity_onCreate` (both were for the legacy NativeActivity
glue path R.2 stopped linking by default).

Migrates the two cross-compile examples that previously carried an
`android_main` trampoline to a minimal `#jni_main #jni_class(...) { }`
stub:

  - `examples/ffi-jni-call-02-void.sx` — tests `#jni_call(void)` lowering
  - `examples/ffi-objc-call-10-os-gate.sx` — tests `inline if OS` gating

Both stubs are empty (no `onCreate` body) so they exercise the
entry-point check + R.3's JNI-symbol synthesis pass produces no
symbols. 131 host / 4 cross / zig build test all green.

`examples/99-android-egl-clear.sx` still uses `android_main` and the
AndroidPlatform/native_app_glue stack — its Android-target build now
fails the entry-point check. R.5 removes it along with the rest of
the legacy NativeActivity surface.
2026-05-20 15:13:33 +03:00
agra
063bbb5419 ffi #jni_main R.3: synthesize JNI-mangled exports for bodied methods
After lowering completes, a new pass walks `foreign_class_map` and, for
every bodied non-static method on a `#jni_main #jni_class("...")` decl,
synthesises a C-ABI exported function whose name follows JNI's name-
mangling convention:

    `Java_<pkg-mangled>_<Class>_sx_1<method-mangled>`

(`/` → `_`, `_` → `_1`). Android's JNI runtime resolves `private native
sx_<method>(...)` declared in the bundled classes.dex via this symbol
without needing an explicit `JNI_OnLoad`/`RegisterNatives` — the
name-mangling fallback is enough.

Param ABI: `(env: *void, self: *void)` prepended (JNIEnv* + jobject
receiver), followed by the user-declared params with pointer types
type-erased to `*void`. The user's body is lowered through the normal
fn-body pipeline with `env`, `self`, and the user-named params bound in
scope. `isExportedEntryName` now also returns true for any name starting
with `Java_` so emit_llvm sets external linkage.

Verified end-to-end: `llvm-nm -D` on the slice 2 smoke .so shows
`Java_co_swipelab_sxjnimain_SxApp_sx_1onCreate` as an exported T
symbol. 131 host / 4 cross / zig build test all green.

Future work (R.3b territory): richer typing inside bodies so `*Self` /
`*Bundle` params support method dispatch through the foreign-class
slot interning. For now `self`/`b` are opaque `*void` jobjects in
scope — fine for stub bodies and `#jni_call`-driven dispatch.
2026-05-20 15:10:33 +03:00
agra
5648337749 ffi #jni_main: accept #jni_main as Android entry; modern smoke shape
Loosens lower.zig's `checkRequiredEntryPoints` to accept either a
`#jni_main #jni_class("...")` decl OR the legacy `android_main`
trampoline. The diagnostic now shows both options when neither is
present.

Updates the slice 2 smoke (`examples/ffi-jni-main-01-emit.sx`) to
express the modern shape — drops `android_main`, declares
`Bundle :: #foreign #jni_class("android/os/Bundle")`, and overrides
`onCreate :: (self: *Self, b: *Bundle) { }` inside the #jni_main class.
The emitted Java now correctly declares `void onCreate(Bundle b)` as
@Override + a matching `private native void sx_onCreate(Bundle b)`
delegate, verified via dexdump.

Full retirement of `android_main` (deleting native_app_glue from the
Android link path, dropping `AndroidPlatform.run_frame_loop`, migrating
chess/EGL demo to the Java-driven lifecycle) is multi-slice rework
and stays as follow-up.
2026-05-20 14:50:21 +03:00
agra
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.
2026-05-20 13:53:25 +03:00
agra
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.
2026-05-20 12:46:40 +03:00
agra
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.
2026-05-20 12:19:15 +03:00
agra
2882748ae6 ffi 2.11 green: DSL call sites on #jni_class-typed receivers lower to JNI dispatch
`inst.method(args)` on a value typed as a foreign-class alias
(`Activity :: #jni_class("android/app/Activity") { getWindow ::
(self: *Self) -> *Window; }` etc.) now lowers to `jni_msg_send`
with descriptor auto-derived from the sx signature, env from the
enclosing `#jni_env` scope (lexical-direct via 2.16b), and slot
interning re-used from Phase 1C.

Touch surface:
- `Lowering` gains `foreign_class_map: StringHashMap(*const
  ForeignClassDecl)` populated in `scanDecls` + `lowerDecls`.
- New `registerForeignClassDecl` records each declared alias; the
  type-bridge fallback already interns the alias as a 0-field
  struct, so `*Activity` resolves cleanly through `getStructTypeName`.
- New `lowerForeignMethodCall` looks up the method in
  `ForeignClassDecl.members`, derives the descriptor via
  `jni_descriptor.deriveMethod` (with a `ClassRegistry` built from
  `foreign_class_map`), and emits `jni_msg_send` directly. Filters
  by runtime — `jni_class`/`jni_interface` lower; `objc_class` etc.
  surface a clear "not yet supported" diagnostic until Phase 3/4.
- `lowerCall`'s method-dispatch arm inserts the foreign-class
  check before the standard struct-method resolution.

JNI descriptor derivation gains `*void → Ljava/lang/Object;` (the
opaque-jobject convention) — common when sx code doesn't have a
precise Java type for the value. Locked in with a unit test.

IR snapshot at `tests/expected/ffi-jni-class-08-call.ir` shows the
full lowering: env from the enclosing fn param, target from the
foreign-class arg, slot-interned `(class, method, sig)` cache
pair, jni_msg_send to `CallObjectMethod` (slot 34). Mangled slot
names `@SX_JNI_CLS_getWindow____Ljava_lang_Object_` confirm the
derived descriptor.

129/129 examples + 16 jni_descriptor unit tests green.
2026-05-20 11:07:41 +03:00
agra
022ca31050 ffi 2.16b green: lexical-direct env in #jni_call inside #jni_env
`Lowering` gains a `jni_env_stack: ArrayList(Ref)`. When lowering
the `jni_env_block` arm pushes the env_expr's Ref before lowering
the body and pops after; `defer` ensures cleanup on early return.

`lowerJniCall` now disambiguates explicit-vs-omitted env via the
position of the first string-literal arg: at index 1 → omitted
(3-arg form `target, "name", "sig"`), at index 2 → explicit
(4-arg form `env, target, "name", "sig"`). Omitted form reads the
top of `jni_env_stack`; missing scope → diagnostic.

End-to-end test runs cleanly. Locked-in IR snapshot at
`tests/expected/ffi-jni-env-02-lexical-direct.ir` shows env coming
from the enclosing fn's `*void` param straight into the jni_msg_send
expansion — no extra load, no thread-local read. The hot-path
optimisation from the design discussion is now real.

128/128 examples + 1 new IR snapshot green; zig test clean.
2026-05-20 10:54:37 +03:00
agra
5bd2c84bb6 ffi 2.16a green: parser + AST + sema for #jni_env(env) { body }
New `hash_jni_env` lexer token; `parsePrimary` dispatches to a small
`parseJniEnvBlock` that consumes `(env) { body }` and returns a new
`JniEnvBlock` AST node (env_expr + body block).

Sema's analyzeNode arm recurses into env + body inside a pushed
scope; findNodeAtOffset descends through both children for go-to-
definition.

Lowering treats it as a syntactic wrapper around the block: env is
evaluated for side effects, body lowers as a normal block. The TL
push/pop semantics (synthesizing the env stack so `#jni_call`'s env
arg can become optional) land in 2.16b.

`expectSemicolonAfter` recognises `jni_env_block` as block-form so
statement-position uses don't need a trailing `;` — matches `if` /
`while` / `for` / bare blocks.

Test runs through the block body and prints expected output; xfail
snapshot flips to green. 127/127 examples green.
2026-05-20 10:41:24 +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
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
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
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
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