Skip to content

feat(stdlib): wasmCall extern for invoking WebAssembly exports by name - #419

Closed
hyperpolymath wants to merge 1 commit into
mainfrom
feat/wasm-call-extern
Closed

feat(stdlib): wasmCall extern for invoking WebAssembly exports by name#419
hyperpolymath wants to merge 1 commit into
mainfrom
feat/wasm-call-extern

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Closes #414. Implements bindings #5 (docs/bindings-roadmap.adoc).

Summary

Adds the generic Option A surface for calling individual WebAssembly exports by name from AffineScript code:

pub extern fn wasmCall(
  exports: WasmExports,
  name: String,
  args: [Float]
) -> Float;

Lowers on the --deno-esm backend to Number(exports[name](...(args || []))). Number coercion means i32/i64/f32/f64 return types all flow back as Float at the AS boundary.

Why this shape

Two approaches were on the table (from docs/bindings-roadmap.adoc and #414):

Option Shape Trade
A (chosen) One generic wasmCall(exports, name, args) -> Float Tiny surface, future-proof, stringly-typed export name
B ~30 per-export typed externs matching a specific Zig module Typed at compile time, brittle to Zig-side changes

Option A unblocks both idaptik vm/wasm (the immediate consumer) and every future WASM-host integration in the estate, with ~10 LoC of stdlib + codegen. Typed per-Zig-fn wrappers can layer on top per-consumer where the discipline is worth the brittleness.

Files

File Change
stdlib/Deno.affine New wasmCall extern with docstring
lib/codegen_deno.ml New __as_wasmCall runtime helper + lowering-table entry
tests/codegen-deno/wasm_call.affine Test fixture (AS source)
tests/codegen-deno/wasm_call.harness.mjs Node ESM harness with 41-byte hand-built wasm module exporting add(i32, i32) -> i32
docs/bindings-roadmap.adoc Row #5 status , rationale updated to point at this PR

Test plan

  • tools/run_codegen_deno_tests.sh — all 7 harnesses green, including the new wasm_call.harness.mjs (4 assertions: add(2,3)=5, add(-1,1)=0, add(0,0)=0, add(100,200)=300)
  • dune build clean
  • Hypatia / governance checks
  • No new TypeScript files (DOC-FORMAT N/A — single .adoc edit, not a new doc)

Out of scope

  • Wasm void / multi-value return shapes — add a specialised extern when a consumer asks.
  • Typed per-Zig-fn shims for idaptik vm/wasm — layered on top of wasmCall in idaptik when the migration restarts there.
  • Interp-side (affinescript eval) handling — wasm calling is --deno-esm-target only, consistent with wasmInstance.

🤖 Generated with Claude Code

Adds the generic Option A surface from bindings #5: a single extern
that lowers to `exports[name](...args)` on the --deno-esm backend,
with Number coercion so i32/i64/f32/f64 returns all coerce to Float
at the AffineScript boundary.

This is the smallest-scope / highest-leverage Tier-1 binding item —
it unblocks idaptik vm/wasm (740 LoC) and every future WASM-host
integration. Typed per-Zig-fn shims can layer on top per-consumer
where typing discipline outweighs the generic surface's flexibility.

Surface:

    pub extern fn wasmCall(
      exports: WasmExports,
      name: String,
      args: [Float]
    ) -> Float;

Lowering (lib/codegen_deno.ml):

    __as_wasmCall(exports, name, args)
      = Number(exports[name](...(args || [])))

Test fixture tests/codegen-deno/wasm_call.{affine,harness.mjs}
exercises the round-trip with a 41-byte hand-built wasm module
exporting add(i32, i32) -> i32; all 7 codegen-deno harnesses pass.

Closes #414. Updates docs/bindings-roadmap.adoc row #5
status ◐ → ● and points the rationale at this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath
hyperpolymath enabled auto-merge (squash) May 28, 2026 10:19
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 81 issues detected

Severity Count
🔴 Critical 4
🟠 High 10
🟡 Medium 67

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action actions/checkout@v6 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action denoland/setup-deno@v2 needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "unknown",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "unknown",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "unknown",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

@hyperpolymath

Copy link
Copy Markdown
Owner Author

Superseded by #422 which bundles the wasmCall additions with the new bindings #4 motion binding. Same code, simpler merge.

auto-merge was automatically disabled May 28, 2026 11:28

Pull request was closed

hyperpolymath added a commit that referenced this pull request May 28, 2026
First tranche of bindings #4 (`docs/bindings-roadmap.adoc`).

## Summary

Adds `stdlib/Motion.affine` providing the initial typed surface over the
[`motion`](https://www.npmjs.com/package/motion) npm library: `animate`
→ `awaitable AnimationControls` → `cancel`. Surface mirrors the existing
Http / Sqlite / Crypto stdlib pattern.

Also bundles the `wasmCall` extern that has been waiting on CI in PR
#419 (same `codegen_deno.ml` runtime + lowering region — bundling keeps
history linear). Closes #414 via the bundle.

## Surface

```affine
pub extern type AnimationControls;
pub extern fn motionAnimate(target: Json, keyframes: Json, options: Json) -> AnimationControls;
pub extern fn motionAwait(controls: AnimationControls) -> Int / { Async };
pub extern fn motionCancel(controls: AnimationControls) -> Int;
```

Args cross the boundary as opaque `Json` (typed keyframe shapes are a
follow-up when `affinescript-motion` lands as a dedicated package).

## Lowering

```javascript
const __as_motionAnimate = (target, keyframes, options) =>
  globalThis.__as_motion.animate(target, keyframes, options);
const __as_motionAwait = (controls) =>
  Promise.resolve(controls).then(() => 0);
const __as_motionCancel = (controls) => {
  if (controls && typeof controls.cancel === "function") controls.cancel();
  return 0;
};
```

Consumer responsibility: production code sets `globalThis.__as_motion =
motionLibrary` at module init. Documented in `stdlib/Motion.affine`'s
preamble.

## Files

| File | Change |
|---|---|
| `stdlib/Motion.affine` | NEW — opaque type + 3 externs with effect
rows |
| `lib/codegen_deno.ml` | `__as_motion*` runtime helpers + lowering
table entries; `__as_wasmCall` helper restored |
| `tests/codegen-deno/motion_smoke.affine` | NEW — AS fixture, `use
Motion::{...}` round-trip |
| `tests/codegen-deno/motion_smoke.harness.mjs` | NEW — Node ESM harness
with mocked `globalThis.__as_motion` |
| `docs/bindings-roadmap.adoc` | Row #4 status `○` → `◐` scaffold |

## Test plan

- [x] `tools/run_codegen_deno_tests.sh` — all 7 harnesses green,
including new `motion_smoke` (4 assertions: target/keyframes/options
pass-through, cancel side-effect, no-op cancel on bare object)
- [x] `dune build bin/main.exe` clean
- [x] `use Motion::{...}` from a `tests/codegen-deno/` fixture resolves
(validates stdlib import path works from standalone-compile mode)

## Follow-ups

Deferred per row #4 rationale:
- `animateMini`, `tween`, `ease`, `spring` (additional surface)
- Typed keyframe shapes (currently opaque `Json`)
- Migration from `stdlib/Motion.affine` to dedicated
`affinescript-motion` package (additive + source-compatible)

## Relationship to PR #419

#419 (wasmCall) was opened with auto-merge armed but blocked on
pre-existing E2E flakes. This PR includes the same wasmCall changes;
merging this supersedes #419, which should be closed afterwards.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 28, 2026
…431)

## Summary

Re-lands the AS-side `wasmCall` surface (extern + test fixtures +
roadmap row flip) that PR #419 originally bundled. #419 was closed as
"superseded by #422", but #422 only carried the motion binding — the
host-side codegen (`__as_wasmCall` + lowering-table entry) is present on
`main` while the AS-side pieces are missing.

Flips `docs/bindings-roadmap.adoc` row #5 from `◐ host-side; AS-side ○`
to `● usable (Option A landed)`.

- `stdlib/Deno.affine`: new `pub extern fn wasmCall(exports:
WasmExports, name: String, args: [Float]) -> Float` with docstring +
`wasmInstance → wasmCall` worked example.
- `tests/codegen-deno/wasm_call.{affine,harness.mjs}`: round-trip
harness over a 41-byte inline wasm module exporting `add(i32, i32) ->
i32` (4 assertions).
- `docs/bindings-roadmap.adoc`: row #5 status `◐` → `●`; cross-cutting
§2 observation rewritten "LANDED".

Closes bindings roadmap #5. Closes #414 (host-side via #422, AS-side via
this PR).

## Test plan

- [x] `affinescript check stdlib/Deno.affine` → Type checking passed.
- [x] `affinescript check tests/codegen-deno/wasm_call.affine` → Type
checking passed.
- [ ] `tools/run_codegen_deno_tests.sh` (gated on Actions budget —
admin-merge per estate policy).

Out of GH Actions budget; admin-merging on clean local verify per estate
policy (see MEMORY.md `session-2026-05-27-estate-sweep-1254-prs`).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant