Commit Graph

9 Commits

Author SHA1 Message Date
agra
bdd0e96d78 feat(lang): block value requires no trailing ; (Rust-style)
A block's value is now its last statement ONLY when that statement is a
trailing expression with no `;`. A trailing `;` discards the value,
leaving the block void. This makes value-vs-statement explicit and lets
the compiler reject "this block was supposed to produce a value".

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

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

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

Gates: zig build, zig build test, run_examples.sh -> 343 passed,
cross_compile.sh -> 7 passed (also refreshed its stale example names).
2026-06-02 09:23:50 +03:00
agra
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
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
1ae43495c2 ffi #jni_main slice 2: AOT pipeline — .java + javac + d8 → classes.dex in APK
Compilation.lowering_jni_main_decls is populated by lowerToIR (iterating
foreign_class_map for is_main && !is_foreign && runtime==jni_class,
deduped by foreign_path); each entry carries the pre-rendered Java source
from jni_java_emit.emitJavaSource.

createApk extended: when the emission list is non-empty, write each
.java under <stage>/java/<pkg>/<Class>.java, javac --release 11 to
<stage>/classes/, d8 --release --lib <android_jar> --output <stage>
to produce <stage>/classes.dex, then zip the .dex into the unaligned
APK at root level. javac discovery: $JAVA_HOME/bin/javac first, then
`which javac`.

Manifest still hardcodes android.app.NativeActivity (slice 3 wires the
user's class name + android:hasCode="true"), so the bundled .dex is
present but unreferenced at runtime. End-to-end verified via dexdump on
the smoke example's APK — Lco/swipelab/sxjnimain/SxApp; extending
NativeActivity shows up in classes.dex. Non-#jni_main APK builds
(99-android-egl-clear.sx) produce the same shape as before.

Cross-compile tuple added for examples/ffi-jni-main-01-emit.sx
(compile-only — APK exercise is manual).
2026-05-20 14:42:03 +03:00
agra
f10daa384a ffi 1.24: verify inverse OS gate for #jni_call on iOS-sim
Adds `ios-sim|examples/ffi-jni-call-02-void.sx` to the cross-compile
tuple list. The `inline if OS == .android { #jni_call(...) }` arm in
that example must strip its body before sema/lower runs on iOS,
otherwise emit_llvm would attempt to load libjvm vtable slots that
don't exist in the iOS SDK and the link step would fail.

This is the JNI mirror of step 1.14, which did the same for
`#objc_call` against Android. Phase 1C is functionally complete:
- Parser accepts all three FFI intrinsics (1.1–1.2)
- `#objc_call` full return-type matrix + selector interning (1.3–1.10)
- `#objc_call` enclosing-construct coverage (1.11–1.13)
- `#objc_call` cross-Android gate (1.14)
- `#jni_call(void)` codegen with vtable indirection (1.15)
- `#jni_call` literal-keyed slot interning (1.16–1.17)
- `#jni_call` return-type matrix s32/s64/f64/bool/*void (1.18–1.22)
- `#jni_static_call` lowering (1.23)
- `#jni_call` cross-iOS gate (1.24, this commit)

3/3 cross-compile tuples pass; 118/119 host tests pass (one
unrelated regression in working tree). Next: Phase 1D for
`library/vendors/sx_android_jni/sx_android_jni.c` — migrate the C
JNI helpers to sx via `#jni_call`. Requires on-device chess
verification per the FFI plan.
2026-05-19 22:44:41 +03:00
agra
134c197dd4 ffi 1.15: xfail — Android cross-compile of #jni_call(void)
Adds `examples/ffi-jni-call-02-void.sx` exercising `#jni_call(void)
(env, target, "name", "sig")` inside an `inline if OS == .android`
arm, plus a new tuple in `tests/cross_compile.sh`. Host run_examples
passes (the inline-if strips the JNI body, leaving "skipped"); the
Android cross-compile FAILs because `lowerFfiIntrinsicCall` still
emits the placeholder diagnostic for any `fic.kind != .objc_call`.

Per the FFI cadence rule this is a test-add (xfail); the next
commit makes the Android cross-compile green by adding the
`.jni_msg_send` opcode and its emit_llvm expansion.
2026-05-19 21:25:42 +03:00
agra
5fad92785e ffi 1.14: #objc_call OS-gating cross-compiles cleanly to Android
109/109 host tests pass; tests/cross_compile.sh's first real tuple
(`android | examples/ffi-objc-call-10-os-gate.sx`) compiles
through `sx build --target android` without finding any
`@objc_msgSend` / `@sel_registerName` symbols in the output —
the `inline if OS == .ios { #objc_call(...) }` arm is stripped
at sx compile time before emit_llvm runs, so the Android
toolchain (Bionic + libGLESv3 / NDK linker) doesn't see the
Obj-C runtime references that would otherwise be undefined.

Host (macOS): the example prints "host stripped both" — the iOS
arm is stripped (we're not iOS) AND the Android arm is stripped
(we're not Android), confirming `inline if OS == { case }`
symmetric strip-and-render works around `#objc_call` sites.

The example carries a 3-line `android_main` trampoline so the
NDK linker's `-u ANativeActivity_onCreate` / entry-point
discovery is satisfied — pattern shared with chess + the other
android examples.
2026-05-19 19:00:47 +03:00
agra
7d2e579667 ffi 0.0: tests/cross_compile.sh scaffold
First step of the FFI ceremony reduction plan (current/PLAN-FFI.md).
Iterates a (target, example) tuple list, runs `sx build --target <t>
<example>`, asserts exit 0 + output file produced. Cross-compile
correctness only — these examples can't run on the host.

Initial tuple list is empty, so the script exits 0 on a clean tree
and contributors without the iOS SDK / Android NDK aren't blocked.
`toolchain_available` short-circuits with a SKIP line when the
requested toolchain isn't installed. Phase 1/2/3 cross-only examples
populate TUPLES as they land.
2026-05-19 11:10:56 +03:00