// 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.

== 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.

For TypeScript projects, the companion `@hyperpolymath/affine-ts` package
provides enhanced declarations with stricter generics.

== 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

PMPL-1.0-or-later.
See link:../../LICENSE[LICENSE] at the repository root.
