// SPDX-License-Identifier: CC-BY-SA-4.0
= affine-js
:toc:
:icons: font

JavaScript interop layer for AffineScript WASM modules.

Part of the https://github.com/hyperpolymath/affinescript[AffineScript] project.

== Installation

[source,json]
----
// deno.json
{
  "imports": {
    "@hyperpolymath/affine-js": "jsr:@hyperpolymath/affine-js@0.1.0"
  }
}
----

== Quick start

[source,javascript]
----
import { AffineModule, run, int } from "@hyperpolymath/affine-js";

// Run a compiled AffineScript program end-to-end:
await run("./hello.wasm");

// Load a module and call individual exports:
const mod = await AffineModule.fromFile("./math.wasm");
const result = mod.call("add", {}, int(3), int(4));
console.log(result); // { kind: "int", value: 7 }
----

== Compiling AffineScript to WASM

[source,bash]
----
# Compile an AffineScript program to WASM:
affinescript compile hello.affine --output hello.wasm

# Compile with WasmGC target (for modern runtimes):
affinescript compile-gc hello.affine --output hello.wasm
----

== Effect handlers

AffineScript programs declare algebraic effects using `effect` declarations.
Every effect operation becomes a WASM import that the host (your JavaScript) must satisfy.

Default handlers are provided for the `IO` effect (print, println, read_line).
Override them via the `imports` option:

[source,javascript]
----
import { AffineModule } from "@hyperpolymath/affine-js";

const mod = await AffineModule.fromFile("./app.wasm", {
  imports: {
    // Override IO.println to send output to a DOM element instead:
    affine_io_println(strPtr) {
      const text = readStringFromMemory(mod.memory, strPtr);
      document.getElementById("output").textContent += text + "\n";
    },

    // Provide a handler for a custom effect:
    // effect Log { fn warn(msg: String); }
    affine_log_warn(strPtr) {
      console.warn("[AffineScript]", readStringFromMemory(mod.memory, strPtr));
    },
  },
});

await mod.runMain();
----

Effect import names follow the pattern `affine_<EffectName>_<opName>` in snake_case.

== Host-agnostic loading (INT-02)

`fromFile` / `run` work on Deno, Node, and in the browser. Relative
specifiers resolve against `options.base` (default: the affine-js module
URL) — pass `import.meta.url` from your own module when the `.wasm` is
relative to *your* file:

[source,javascript]
----
const mod = await AffineModule.fromFile("./app.wasm", {
  base: import.meta.url,
});
----

`http(s):`, `data:`, and `blob:` URLs are fetched; `file:` URLs are read
via the native filesystem on Deno/Node and via `fetch` in the browser.
Windows and POSIX absolute paths are both accepted.

== Cross-module imports (INT-01)

`use Mod::{fn}` lowers to a WASM import under the `Mod` namespace, not
`env`. Supply per-namespace implementations with `options.modules`:

[source,javascript]
----
const mod = await AffineModule.fromBytes(bytes, {
  modules: { Mod: { helper: (x) => x + 1 } },
});
----

== Ownership section (typed-wasm contract)

The compiler embeds an `typedwasm.ownership` custom section carrying
per-function parameter/return ownership kinds (the AffineScript ↔
typed-wasm contract — see `docs/ECOSYSTEM.adoc`). Read it via:

[source,javascript]
----
mod.ownership;
// => [{ funcIdx, paramKinds: ["linear", ...], retKind: "unrestricted" }, ...]
----

== AffineValue types

All JS ↔ WASM value exchanges use the `AffineValue` tagged-union type:

[cols="1,2,2"]
|===
| Kind | Construction | Description

| `int`
| `int(42)`
| 32-bit integer

| `float`
| `float(3.14)`
| 64-bit float

| `bool`
| `bool(true)`
| Boolean

| `unit`
| `unit()`
| Unit / void

| `string`
| `string("hello")`
| UTF-8 string (heap-allocated in WASM memory)

| `some`
| `some(int(5))`
| `Option[T]::Some(v)`

| `none`
| `none()`
| `Option[T]::None`

| `ok`
| `ok(string("result"))`
| `Result[T,E]::Ok(v)`

| `err`
| `err(string("oops"))`
| `Result[T,E]::Err(e)`

| `array`
| `array([int(1), int(2)])`
| Homogeneous array

| `record`
| `record({ x: int(1), y: int(2) })`
| Structural record
|===

== TypeScript support

`affine-js` ships `types.d.ts` declarations.  TypeScript projects can use the
package without any additional steps — the type definitions are automatically
picked up by `tsc` and editors.

TypeScript is not a first-class consumer in the AffineScript ecosystem. There
is no `@hyperpolymath/affine-ts` package; TS callers should consume
`@hyperpolymath/affine-js` directly via the shipped `.d.ts` and supply any
project-specific typings themselves.

== Return type hints

Because WASM erases type information at the binary level, you need to tell the
unmarshaler how to interpret return values for complex types:

[source,javascript]
----
// Simple types — no hint needed (defaults to "int"):
mod.call("get_count");        // → { kind: "int", value: 42 }

// Hint for strings:
mod.call("get_name", { returnType: "string" });

// Hint for Option:
mod.call("maybe_find", { returnType: "option" });

// Hint for Result:
mod.call("try_parse", { returnType: "result" });
----

== Memory layout

The AffineScript WASM runtime uses the following in-memory representation:

- **Int** → `i32` WASM value
- **Float** → `f64` WASM value
- **Bool** → `i32` (0 = false, 1 = true)
- **Unit** → `i32` sentinel (0)
- **String** → pointer to `[length: i32][...utf-8 bytes]`
- **Option::None** → null pointer (0)
- **Option::Some(v)** → pointer to `[TAG_SOME: i32][...payload]`
- **Result::Ok(v)** → pointer to `[TAG_OK: i32][...payload]`
- **Result::Err(e)** → pointer to `[TAG_ERR: i32][...payload]`
- **Array[T]** → pointer to `[length: i32][...element pointers (i32)]`
- **Record** → pointer to consecutive field values (in declaration order)

Tag constants are exported from `@hyperpolymath/affine-js/runtime` as `AFFINE_TAG`.

== License

MPL-2.0.
See link:../../LICENSE[LICENSE] at the repository root.
