Skip to content

feat(cli,launcher): Ephapax String FFI + subcommand-name dispatch#34

Merged
hyperpolymath merged 1 commit into
feat/cli-port-15b-main-ephfrom
feat/cli-launcher-string-bridge
May 20, 2026
Merged

feat(cli,launcher): Ephapax String FFI + subcommand-name dispatch#34
hyperpolymath merged 1 commit into
feat/cli-port-15b-main-ephfrom
feat/cli-launcher-string-bridge

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Stacked PR. Base is `feat/cli-port-15b-main-eph` (#33). When that merges, this PR's base auto-rebases.

Summary

Resolves the three v2-grammar limits #33 documented by adding three small launcher bridges, then upgrades Main.eph to use them — turning the argv-count classifier into a real argv[0] subcommand-name dispatcher.

Launcher bridges added

Bridge Signature Purpose
`ephStringSlice(env, handle)` (helper) host fn Walks Ephapax's String layout (`handle` → 8-byte header → `data_ptr` + `len`) to recover the byte slice from a single i32 String handle.
`env::say_string(s: String)` `(i32) → ()` Print an Ephapax String via stderr. Mirrors baseline `print_string` but accepts the high-level type.
`env::argv_eq_string(idx: I32, literal: String)` `(i32, i32) → i32` Returns 1 if `argv[idx]` equals the literal. Unblocks subcommand-name dispatch entirely.
`env::i64_is_zero(n: I64)` `(i64) → i32` Works around v2-grammar Ephapax's missing i64 literal. `cap_token` comparison now typechecks.

Main.eph upgrade

  • Subcommand classifier now matches argv[0] against the real names: version / info / dev / build / run / bundle / init. 9-variant sum type covers all + NoArg + Unknown.
  • Per-subcommand `announce(s)` prints a recognisable banner via `say_string` — round-trips Ephapax literals through linear memory back to host stderr.
  • `capsLive()` actually runs now (stubbed before) — calls `i64_is_zero` against the eager-granted FileSystem token.
  • Composite `statusCode` = dispatch + (grooves×10) + caps. e.g. `gossamer dev` with 2 grooves and caps OK → 221.

Wasm shape (verified by Import section dump)

Compiles to 3121-byte cli.wasm, up from 1514. 9 user imports + 2 ephapax baseline. Every signature matches the launcher's bridge surface byte-for-byte:

```
env::say_string type_idx=1 (i32) -> ()
env::argv_eq_string type_idx=3 (i32, i32) -> i32
env::cap_token type_idx=7 (i32) -> i64
env::i64_is_zero type_idx=8 (i64) -> i32
env::print_i32 type_idx=1 (i32) -> ()
env::argv_count type_idx=0 () -> i32
env::gossamer_groove_discover type_idx=0 () -> i32
... + 2 baseline
```

What this unlocks

Subcommand dispatch with the conventional argv[0]-match shape is the bedrock the rest of the port needs. The remaining gap is just filling in the per-subcommand bodies — opening webviews, running watchers, calling shell — which is now an "exhaustive bridge coverage" task rather than a "does this approach work at all" question.

Test plan

  • `zig ast-check` on `bridges.zig` — clean.
  • `ephapax compile cli/src/Main.eph -o /tmp/cli-v3.wasm` — 3121 bytes, all 9 user imports present with correct signatures.
  • Reviewer: with libwasmtime installed and libgossamer built, run `zig build run` under `cli/launcher/` with various argv (e.g. `-- dev`, `-- info`, `-- nonsense`) and observe banners + status codes.

🤖 Generated with Claude Code

Resolves the three v2-grammar limits documented in the previous
Main.eph commit by adding three small launcher bridges and using them
to upgrade the dispatcher from argv-count classification to real
argv[0] subcommand-name matching.

Launcher bridges added (cli/launcher/src/bridges.zig):

  • ephStringSlice(env, handle) helper — walks Ephapax's String layout
    (8-byte header at handle: { data_ptr: i32, len: i32 }) to recover
    the underlying byte slice from a single i32 String handle.

  • env::say_string(s: String) -> ()
      Takes an Ephapax String handle, prints its bytes to stderr.
      Mirrors the baseline env::print_string but accepts the high-level
      String type so .eph code calls it with a literal:
        say_string("hello")

  • env::argv_eq_string(idx: I32, literal: String) -> I32
      Returns 1 if argv[idx] equals the literal byte-for-byte.
      Unblocks subcommand-name dispatch entirely.

  • env::i64_is_zero(n: I64) -> I32
      Works around v2-grammar Ephapax's lack of an i64 literal
      (integer literals always parse as I32 unless > i32::MAX). Now
      cap_token comparison + opaque-handle nullability checks
      typecheck.

Main.eph upgrade (cli/src/Main.eph):

  • Subcommand classifier now matches argv[0] against the real names:
    version / info / dev / build / run / bundle / init. 9-variant
    sum type covers all + NoArg + Unknown.

  • Per-subcommand `announce(s: Subcommand)` prints a recognisable
    banner via say_string — proves the new String-aware bridge round-
    trips Ephapax literals through linear memory back to host stderr.

  • capsLive() now actually runs (used to be stubbed out) — calls
    i64_is_zero against the eager-granted FileSystem token.

  • Composite statusCode = dispatch + (grooves*10) + caps.
    e.g. `gossamer dev` with 2 grooves and caps OK -> 221.

Compiles to 3121-byte cli.wasm (up from 1514). 9 user imports plus
2 ephapax baseline — all 9 match the launcher's bridge surface
signatures byte-for-byte (verified via wasm Import section dump).

What this unlocks:

  Subcommand dispatch with the conventional argv[0]-match shape is
  the bedrock the rest of the port needs. The remaining gap is just
  filling in the per-subcommand bodies — opening webviews, running
  watchers, calling shell, etc. — which is now an "exhaustive bridge
  coverage" task rather than a "does this approach work at all"
  question.

Verified:

  • zig ast-check on bridges.zig — clean.
  • ephapax compile cli/src/Main.eph -o /tmp/cli-v3.wasm — 3121 bytes,
    9 user imports + 2 baseline, signatures match launcher bridges:
        env::say_string         type_idx=1   (i32) -> ()
        env::argv_eq_string     type_idx=3   (i32, i32) -> i32
        env::cap_token          type_idx=7   (i32) -> i64
        env::i64_is_zero        type_idx=8   (i64) -> i32

Stacked on #33 (the Main.eph baseline). Base set accordingly. When
both this and #33 land, only the per-subcommand bodies remain for the
final-port PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath merged commit e40acc5 into feat/cli-port-15b-main-eph May 20, 2026
13 of 14 checks passed
hyperpolymath added a commit that referenced this pull request May 20, 2026
* feat(cli): Main.eph subcommand dispatcher in v2-grammar Ephapax

Replaces the 14a.5c placeholder (which only called
print_i32(argv_count)) with a real dispatcher that exercises the v2
grammar — sum types, pattern matching, multi-branch conditionals, let
bindings, multiple FFI extern declarations, and real calls into
libgossamer through the 14a.5b bridges.

Architecture:

  • pub data Subcommand = NoArg | InfoOrVersion | DevOrRunOrBuild |
    Init | Bundle | TooMany — classifies the run by argv_count.
  • dispatchCode(s) match-lowers each variant to a stable status code
    (100..900). Decodes from outside.
  • grooveProbe() calls env::gossamer_groove_discover — proves the
    14a.5b libgossamer bridge surface is reachable from Ephapax.
  • statusCode() composes dispatch + clamped-groove-count into a
    single I32 so a wrapper / test harness reads back the full state
    in one print_i32 call.
  • main() prints statusCode() and returns.

Compiles to 1514-byte cli.wasm. 5 host imports (all in the launcher's
bridge surface from 14a.5a/5b): print_i32, argv_count, argv_arg_len,
gossamer_groove_discover, gossamer_groove_status. Plus the 2 always-on
ephapax baseline imports (print_i32, print_string) — the 2nd print_i32
collapses with the user-declared one at link time since both are
env::print_i32 with the same (i32)->() signature.

What's intentionally deferred:

  • Subcommand-name dispatch (match argv[1] against "dev"|"build"|...).
    Requires linear-memory reads which v2-grammar Ephapax doesn't yet
    expose at the source level. Workaround in this PR: dispatch on
    argv_count instead. The conventional argv[1]-match becomes a
    follow-up once the stdlib gains a string-from-bytes helper.

  • Capability-token liveness check. The launcher's env::cap_token
    returns I64; v2-grammar Ephapax has no I64 literal (integer
    literals always parse as I32 unless they exceed i32::MAX), so
    "if cap_token(0) == 0" can't typecheck. Three documented
    workarounds in the file comment; (a) add env::i64_is_zero helper
    is the picked path when we revisit.

  • String-typed FFI calls. Declaring `extern "env" { fn foo(s: String) }`
    produces a wasm import with single-i32 signature carrying an opaque
    Ephapax String handle. The launcher's existing bridges take
    (ptr: i32, len: i32) pairs — different signature. Resolving means
    either landing a launcher-side handle-deref bridge or routing
    through Ephapax's __ephapax_string_len + memory exports. Tracked
    for the follow-up that ports cmdDev / cmdBuild / etc.

Verified:

  • ephapax compile cli/src/Main.eph -o /tmp/cli-v2.wasm — 1514 bytes,
    7 imports (5 baseline + 5 user, with 1 duplicate name absorbed
    by the linker), 13 exports (7 runtime helpers + memory + 5 user
    functions including main).
  • Wasm Import section names match the launcher's bridge surface
    (cli/launcher/src/main.zig + bridges.zig) exactly.

#15 step 2 of the gossamer CLI port. Stacked on the launcher chain
(14a.5a/b/c, gossamer #28/29/30); base is the most recent stack head.
The actual subcommand bodies — opening windows, running watcher, etc. —
follow in a #15 step 3 once the string-FFI gap is resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli,launcher): Ephapax String FFI + subcommand-name dispatch (#34)

Resolves the three v2-grammar limits documented in the previous
Main.eph commit by adding three small launcher bridges and using them
to upgrade the dispatcher from argv-count classification to real
argv[0] subcommand-name matching.

Launcher bridges added (cli/launcher/src/bridges.zig):

  • ephStringSlice(env, handle) helper — walks Ephapax's String layout
    (8-byte header at handle: { data_ptr: i32, len: i32 }) to recover
    the underlying byte slice from a single i32 String handle.

  • env::say_string(s: String) -> ()
      Takes an Ephapax String handle, prints its bytes to stderr.
      Mirrors the baseline env::print_string but accepts the high-level
      String type so .eph code calls it with a literal:
        say_string("hello")

  • env::argv_eq_string(idx: I32, literal: String) -> I32
      Returns 1 if argv[idx] equals the literal byte-for-byte.
      Unblocks subcommand-name dispatch entirely.

  • env::i64_is_zero(n: I64) -> I32
      Works around v2-grammar Ephapax's lack of an i64 literal
      (integer literals always parse as I32 unless > i32::MAX). Now
      cap_token comparison + opaque-handle nullability checks
      typecheck.

Main.eph upgrade (cli/src/Main.eph):

  • Subcommand classifier now matches argv[0] against the real names:
    version / info / dev / build / run / bundle / init. 9-variant
    sum type covers all + NoArg + Unknown.

  • Per-subcommand `announce(s: Subcommand)` prints a recognisable
    banner via say_string — proves the new String-aware bridge round-
    trips Ephapax literals through linear memory back to host stderr.

  • capsLive() now actually runs (used to be stubbed out) — calls
    i64_is_zero against the eager-granted FileSystem token.

  • Composite statusCode = dispatch + (grooves*10) + caps.
    e.g. `gossamer dev` with 2 grooves and caps OK -> 221.

Compiles to 3121-byte cli.wasm (up from 1514). 9 user imports plus
2 ephapax baseline — all 9 match the launcher's bridge surface
signatures byte-for-byte (verified via wasm Import section dump).

What this unlocks:

  Subcommand dispatch with the conventional argv[0]-match shape is
  the bedrock the rest of the port needs. The remaining gap is just
  filling in the per-subcommand bodies — opening webviews, running
  watchers, calling shell, etc. — which is now an "exhaustive bridge
  coverage" task rather than a "does this approach work at all"
  question.

Verified:

  • zig ast-check on bridges.zig — clean.
  • ephapax compile cli/src/Main.eph -o /tmp/cli-v3.wasm — 3121 bytes,
    9 user imports + 2 baseline, signatures match launcher bridges:
        env::say_string         type_idx=1   (i32) -> ()
        env::argv_eq_string     type_idx=3   (i32, i32) -> i32
        env::cap_token          type_idx=7   (i32) -> i64
        env::i64_is_zero        type_idx=8   (i64) -> i32

Stacked on #33 (the Main.eph baseline). Base set accordingly. When
both this and #33 land, only the per-subcommand bodies remain for the
final-port PR.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 20, 2026
…-targets PR #34 at main) (#39)

* feat(cli): Main.eph subcommand dispatcher in v2-grammar Ephapax

Recovery PR 3/4. Same content as orphaned PR #33 — a dispatcher
that exercises sum types, pattern matching, multi-branch
conditionals, let bindings, multiple FFI externs, and real calls
into libgossamer through the 14a.5b bridges (now on main via #35).

Architecture:

  • pub data Subcommand = NoArg | InfoOrVersion | DevOrRunOrBuild |
    Init | Bundle | TooMany — classifies the run by argv_count.
  • dispatchCode(s) match-lowers each variant to a stable status
    code (100..900).
  • grooveProbe() calls env::gossamer_groove_discover — proves the
    14a.5b libgossamer bridge surface is reachable from Ephapax.
  • statusCode() composes dispatch + clamped-groove-count into a
    single I32 the harness reads back.
  • main() prints statusCode() and returns.

Compiles to 1514-byte cli.wasm. 5 host imports (all in the launcher's
bridge surface from 14a.5a/5b): print_i32, argv_count, argv_arg_len,
gossamer_groove_discover, gossamer_groove_status. Plus the 2 always-on
ephapax baseline imports.

Documented v2-grammar limits hit during this work (all three
addressed in the next recovery PR via launcher-side helpers):

  1. No linear-memory reads from user code (no argv[1] string match).
  2. No I64 literal (cap_token == 0 can't typecheck).
  3. String-typed externs lower to opaque i32 handles, not (ptr, len).

Subcommand dispatch via argv_count is the workaround in this PR;
the conventional argv[0]-match shape lands in recovery PR 4/4 with
the String FFI bridges.

Recovery PR 3/4. Lands on top of:
  • #35 (14a.5b libgossamer bridges) — MERGED
  • #37 (14a.5c build integration) — open, will merge before this

This PR's Main.eph is dead code without #37's build.zig ephapax
compile step. Merge order matters: #37 first, then this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(cli,launcher): Ephapax String FFI + subcommand-name dispatch

Resolves the three v2-grammar limits documented in the previous
Main.eph commit by adding three small launcher bridges and using them
to upgrade the dispatcher from argv-count classification to real
argv[0] subcommand-name matching.

Launcher bridges added (cli/launcher/src/bridges.zig):

  • ephStringSlice(env, handle) helper — walks Ephapax's String layout
    (8-byte header at handle: { data_ptr: i32, len: i32 }) to recover
    the underlying byte slice from a single i32 String handle.

  • env::say_string(s: String) -> ()
      Takes an Ephapax String handle, prints its bytes to stderr.
      Mirrors the baseline env::print_string but accepts the high-level
      String type so .eph code calls it with a literal:
        say_string("hello")

  • env::argv_eq_string(idx: I32, literal: String) -> I32
      Returns 1 if argv[idx] equals the literal byte-for-byte.
      Unblocks subcommand-name dispatch entirely.

  • env::i64_is_zero(n: I64) -> I32
      Works around v2-grammar Ephapax's lack of an i64 literal
      (integer literals always parse as I32 unless > i32::MAX). Now
      cap_token comparison + opaque-handle nullability checks
      typecheck.

Main.eph upgrade (cli/src/Main.eph):

  • Subcommand classifier now matches argv[0] against the real names:
    version / info / dev / build / run / bundle / init. 9-variant
    sum type covers all + NoArg + Unknown.

  • Per-subcommand `announce(s: Subcommand)` prints a recognisable
    banner via say_string — proves the new String-aware bridge round-
    trips Ephapax literals through linear memory back to host stderr.

  • capsLive() now actually runs (used to be stubbed out) — calls
    i64_is_zero against the eager-granted FileSystem token.

  • Composite statusCode = dispatch + (grooves*10) + caps.
    e.g. `gossamer dev` with 2 grooves and caps OK -> 221.

Compiles to 3121-byte cli.wasm (up from 1514). 9 user imports plus
2 ephapax baseline — all 9 match the launcher's bridge surface
signatures byte-for-byte (verified via wasm Import section dump).

What this unlocks:

  Subcommand dispatch with the conventional argv[0]-match shape is
  the bedrock the rest of the port needs. The remaining gap is just
  filling in the per-subcommand bodies — opening webviews, running
  watchers, calling shell, etc. — which is now an "exhaustive bridge
  coverage" task rather than a "does this approach work at all"
  question.

Verified:

  • zig ast-check on bridges.zig — clean.
  • ephapax compile cli/src/Main.eph -o /tmp/cli-v3.wasm — 3121 bytes,
    9 user imports + 2 baseline, signatures match launcher bridges:
        env::say_string         type_idx=1   (i32) -> ()
        env::argv_eq_string     type_idx=3   (i32, i32) -> i32
        env::cap_token          type_idx=7   (i32) -> i64
        env::i64_is_zero        type_idx=8   (i64) -> i32

Stacked on #33 (the Main.eph baseline). Base set accordingly. When
both this and #33 land, only the per-subcommand bodies remain for the
final-port PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath deleted the feat/cli-launcher-string-bridge branch May 20, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant