feat(cli,launcher): Ephapax String FFI + subcommand-name dispatch#34
Merged
hyperpolymath merged 1 commit intoMay 20, 2026
Merged
Conversation
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>
e40acc5
into
feat/cli-port-15b-main-eph
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>
This was referenced May 20, 2026
Merged
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Main.eph upgrade
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
🤖 Generated with Claude Code