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-rssourcegen —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 Rustnear-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 inProofForge/Target/Registry.lean (def wasmNear):
Deviations from Original Plan
The implementation diverged from the original plan in several places, all documented in D-019:-
Map keys widened to Hash. The plan specified only
Map<U64, …>, butMapProbeusesMap<Hash, Hash, 128>. The implementation supports both U64 and Hash keys with separate__pf_map_key_u64/__pf_map_key_hashhelpers. -
.assertionsand.accountExplicitadded. The plan’s capability list omitted these, butMapProbeusesassertEqandContextProbeusescontractId. Both are now in the profile and lowered. -
.crosscallInvokeremoved. As planned, not supported in v0 sourcegen. -
assert/assertEqlowered. Since.assertionsis in the profile, these lower to Rustassert!/assert_eq!macros rather than being rejected. -
ifElse/boundedForrejected. Not in the capability profile. -
nativeValueexpression rejected. Despite.valueNativebeing 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 onnear-sdk = "5",borsh = "1",serde = "1",serde_json = "1"src/lib.rs—#[near(contract_state)]struct,Defaultimpl,#[near] implblock with entrypoints, and conditional helper functions
Scalar State
Scalar state fields become Rust struct fields withBorshDeserialize/BorshSerialize:
Map State
Map state uses raw NEAR KV storage throughenv::storage_read,
env::storage_write, and env::storage_has_key. Key helpers are emitted per
key type used:
__pf_map_key_u64(prefix, key)— forMap<U64, …>__pf_map_key_hash(prefix, key)— forMap<Hash, …>
__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-unknowntargetnear-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
EmitWatpath instead. - Should
nativeValueexpression inspection (attached deposit) be added as a dedicated IR context field on the canonical path? - Should map storage use
near_sdk::collections::LookupMapin the v0 reference, or leave rawenv::storage_read/env::storage_writeas the documented semantics thatEmitWatmust reproduce?