Skip to main content
Target id: wasm-near Family: Wasm host Two backends coexist:
  • Canonical (target state): EmitWat — Portable IR → Wasm AST → WAT → wat2wasm, mirroring the in-repo portable-IR → Yul EVM backend. See D-031 and Wasm family common shape.
  • Frozen v0 stopgap (in-repo, compiles): Rust near-sdk-rs sourcegen — Portable IR → near-sdk-rs package → cargo wasm32. Validates NEAR semantics now; not expanded. Its details are documented below under Frozen v0 reference.

Canonical architecture (EmitWat)

EmitWat mirrors ProofForge/Backend/Evm/IR.lean (portable IR → Yul AST), but targets WAT instead of Yul. Because the portable IR already abstracts over Lean objects (only u32/u64/bool/hash scalars + storage effects), there is no Lean runtime to port and no object-model boxing/GC — scalars map directly to Wasm i32/i64, and storage/crypto/context effects lower to NEAR host imports. The IR-lowering and validation logic is reusable from the frozen Rust v0 (Backend/WasmNear/IR.lean) and from Backend/Evm/IR.lean; only the emission target changes (Rust/Yul strings → Wasm AST → WAT). Storage/crypto/context effects lower to these NEAR host imports:

Why not EmitZig

The earlier plan (Lean → EmitZig → Zig → host bridge → Wasm) is superseded because it requires porting the full Lean runtime to Wasm (libuv / threads / GC) — the documented blocker. EmitWat lowers the portable IR directly and avoids that port entirely; it also avoids coupling to near-sdk macros (the source of the E0119 / missing-&self bugs in the Rust v0).

Spike gate (highest risk) — RESOLVED (EmitWat end-to-end)

NEAR passes entrypoint arguments as serialized Borsh and expects serialized returns; contract methods export as () -> () dispatchers that read args via env.input()/env.read_register and return via env.value_return (not wasm function returns). The risk is fully de-risked, not just by the hand-written reference counter (examples/near/spike/handwritten-counter.wat, ~40 lines) but by the complete EmitWat backend lowering real IR modules end-to-end. The ABI is symmetric Borsh: params decode from env.input (u32/u64/bool/hash, packed LE at their cumulative Borsh offset) and returns encode via value_return of LE bytes (u32/u64/bool) or the 32-byte hash directly — matching near-sdk-rs’s Borsh convention, no JSON. 7 IR example probes deploy to near-sandbox and pass their scenarios via viewRaw + Borsh decode (Counter / Features / Map / Hash / Context / Params / Event), plus a Map<Hash,Hash> smoke (hash-keyed map) and a u32 arithmetic smoke that exercises .pow (17^2=289 asserted). The four CLI emit modes (--emit-{counter,context,hash,map}-emitwat -o <dir>) lower the built-in IR examples and write <name>.wat + <name>.wasm (via wat2wasm) — a deploy-ready package with no cargo step. The foundational risk is resolved: the register-based host ABI and Borsh (de)serialization are tractable at the WAT level for real IR, not just a hand-written counter.

Frozen v0 reference (Rust sourcegen)

The remainder of this document describes the frozen Rust near-sdk-rs sourcegen backend (ProofForge/Backend/WasmNear/IR.lean). It is kept as a working reference that validates NEAR semantics and capability coverage; it is not the canonical path and is not being expanded. Backend pattern: Portable IR → Rust near-sdk-rs package → cargo build --target wasm32-unknown-unknown → NEAR-compatible Wasm.

Capability Profile

Defined in ProofForge/Target/Registry.lean (def wasmNear):

Deviations from Original Plan

The implementation diverged from the original plan in several places, all documented in D-019:
  1. Map keys widened to Hash. The plan specified only Map<U64, …>, but MapProbe uses Map<Hash, Hash, 128>. The implementation supports both U64 and Hash keys with separate __pf_map_key_u64/__pf_map_key_hash helpers.
  2. .assertions and .accountExplicit added. The plan’s capability list omitted these, but MapProbe uses assertEq and ContextProbe uses contractId. Both are now in the profile and lowered.
  3. .crosscallInvoke removed. As planned, not supported in v0 sourcegen.
  4. assert/assertEq lowered. Since .assertions is in the profile, these lower to Rust assert!/assert_eq! macros rather than being rejected.
  5. ifElse/boundedFor rejected. Not in the capability profile.
  6. nativeValue expression rejected. Despite .valueNative being in the profile (for the capability declaration), the expression for inspecting attached deposit is not supported in v0.

Generated Package Structure

renderPackage produces a NearPackage with these files:
  • Cargo.toml — package name sanitized from module name, dependencies on near-sdk = "5", borsh = "1", serde = "1", serde_json = "1"
  • src/lib.rs#[near(contract_state)] struct, Default impl, #[near] impl block with entrypoints, and conditional helper functions

Scalar State

Scalar state fields become Rust struct fields with BorshDeserialize/BorshSerialize:

Map State

Map state uses raw NEAR KV storage through env::storage_read, env::storage_write, and env::storage_has_key. Key helpers are emitted per key type used:
  • __pf_map_key_u64(prefix, key) — for Map<U64, …>
  • __pf_map_key_hash(prefix, key) — for Map<Hash, …>
Codec helpers are emitted only for value types actually used by the module: __pf_encode_u64/__pf_decode_u64, __pf_encode_bool/__pf_decode_bool, __pf_encode_hash/__pf_decode_hash. Map set helpers (__pf_map_set_u64, __pf_map_set_bool, __pf_map_set_hash) return the previous value, matching existing Psy/EVM map semantics.

Context Fields

The __pf_account_id_hash_u64 helper is emitted when either .userId or .contractId is used.

Hash Helpers

Emitted when .cryptoHash is used:
  • __pf_hash(value: [u64; 4]) -> [u64; 4] — single-value SHA-256
  • __pf_hash_two_to_one(left: [u64; 4], right: [u64; 4]) -> [u64; 4] — two-to-one SHA-256

Events

eventEmit lowers to near_sdk::log! with deterministic JSON. v0 supports U32, U64, Bool, and Hash event fields.

CLI Modes

-o is required for wasm-near modes and is interpreted as a package output directory (not a single file). Existing EVM/Psy -o behavior is unchanged.

Implementation Files

Canonical (EmitWat): Frozen v0 reference (Rust sourcegen):

Required Tools

  • rustup + cargo + wasm32-unknown-unknown target
  • near-cli (for deployment validation; not required for source generation or cargo build)

Verification

Open Questions

These concern the frozen v0 (Rust sourcegen). The canonical-path open questions live in Wasm family common shape.
  • The v0 is frozen; capability coverage is not being expanded on the Rust route. New capabilities land on the canonical EmitWat path instead.
  • Should nativeValue expression inspection (attached deposit) be added as a dedicated IR context field on the canonical path?
  • Should map storage use near_sdk::collections::LookupMap in the v0 reference, or leave raw env::storage_read/env::storage_write as the documented semantics that EmitWat must reproduce?