Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

EncDSL v4 — Language Reference

Complete specification. Every construct, every compilation rule, every primitive.

Source of truth: spec-lean/Enc/DSL/Grammar.lean


Table of Contents

  1. Design Principles
  2. Core Invariants
  3. Value Domain
  4. Expression Grammar (14 forms)
  5. Statement Grammar (7 forms)
  6. Built-in Operations (21 ops)
  7. Typed Primitive Registry (30 primitives)
  8. String-based Calls
  9. Raw Expression Escape Hatch
  10. Module IR
  11. Compilation Rules — JavaScript
  12. Compilation Rules — Rust
  13. Evaluator Semantics
  14. Effect System
  15. Trust Surface
  16. Standard Library Derivations
  17. Termination
  18. Proof Inventory
  19. Generated File Inventory
  20. Worked Examples

1. Design Principles

  1. Minimal complete basis — 14 expression forms, each irreducible. No form is derivable from the others except via encoding.
  2. Backend portability — Every form has a direct lowering to JavaScript and Rust. No backend-specific constructs in the core grammar.
  3. Sandbox purity by construction — No ambient access constructors. The grammar cannot express Date.now(), fetch(), or Math.random(). All external input enters through function parameters.
  4. Typed trust boundary — 30 primitives are enumerated in a closed Prim type. The compiler knows every primitive by name. No string-based dispatch for trusted operations.
  5. Effects as data — Side effects are records pushed to an ordered trace. The kernel never executes them. The host interprets them.
  6. Agent generation target — Small, orthogonal, uniform. An LLM can generate correct EncDSL because the grammar is small enough to fit in a prompt.

2. Core Invariants

These hold for every well-formed EncDSL program:

  • Expressions are total and deterministic. Every expression evaluates to a value. No exceptions, no divergence, no undefined behavior.
  • Statements cannot perform ambient effects directly. The only way to cause a side effect is emit, which pushes a record onto the effect trace.
  • Effects are explicit data. The effect trace is an ordered list of records. The kernel returns it; the host interprets it.
  • Generated kernel behavior depends only on explicit inputs. Same (state, event, context) → same (newState, effects).
  • Any target backend must preserve canonical serialization and effect ordering. A conforming Rust implementation produces byte-identical output to the JavaScript implementation for identical inputs.

3. Value Domain

Val ::= nat   Nat           -- unbounded natural number
      | bool  Bool          -- true or false
      | str   String        -- UTF-8 string
      | bytes List<UInt8>   -- byte array
      | list  List<Val>     -- ordered list of values
      | record List<(String × Val)>  -- association list (key-value pairs)
      | none                -- absence (null)

Lean definition

inductive Val where
  | nat (n : Nat)
  | bool (b : Bool)
  | str (s : String)
  | bytes (bs : List UInt8)
  | list (vs : List Val)
  | record (fields : List (String × Val))
  | none

Backend representations

ValJavaScriptRust
nat 424242
bool truetruetrue
str "hello"'hello'"hello".to_string()
bytes [0x01, 0x02]new Uint8Array([1, 2])vec![1u8, 2u8]
list [nat 1, nat 2][1, 2]vec![1, 2]
record [("a", nat 1)]{ a: 1 }HashMap::from([("a".to_string(), 1)])
nonenullNone

Notes

  • Single number type. No BigInt/Number distinction at the DSL level. Backend decides representation. JavaScript uses Number for small values and BigInt for bitmask operations (injected via rawExpr or Prim.bigInt/Prim.number).
  • No closure values. Lambdas exist in the expression grammar but eval returns none for closures. Closures are compilation-only constructs — they compile to JS/Rust closures but have no first-class representation in the value domain.

4. Expression Grammar (14 forms)

Every expression has a unique constructor in the Expr inductive type. The 14 forms are:

4.1 var — Variable Reference

Syntax:    var(name)
Lean:      .var (name : String)
Semantics: Look up `name` in the environment. Returns `none` if unbound.
JS:        name
Rust:      name

Example: .var "x" → JS: x → Rust: x

Special handling in Rust: JS-specific literals in variable names are translated:

  • 0xFFn0xFFu128
  • 1n1u128
  • =====

4.2 lit — Literal Value

Syntax:    lit(val)
Lean:      .lit (val : Val)
Semantics: Return the literal value unchanged.
JS:        (see Val compilation table in §3)
Rust:      (see Val compilation table in §3)

Examples:

  • .lit (.nat 42) → JS: 42 → Rust: 42
  • .lit (.bool true) → JS: true → Rust: true
  • .lit (.str "hello") → JS: 'hello' → Rust: "hello".to_string()
  • .lit (.none) → JS: null → Rust: None
  • .lit (.list [.nat 1, .nat 2]) → JS: [1, 2] → Rust: vec![1, 2]
  • .lit (.record [("a", .nat 1), ("b", .nat 2)]) → JS: { a: 1, b: 2 } → Rust: HashMap::from([...])

4.3 lam — Lambda Abstraction

Syntax:    lam(param, body)
Lean:      .lam (param : String) (body : Expr)
Semantics: Opaque in eval (returns none). Compilation-only construct.
JS:        (param) => body
Rust:      |param| body

Example: .lam "x" (.op .add [.var "x", .lit (.nat 1)]) → JS: (x) => (x + 1) → Rust: |x| (x + 1)

4.4 app — Function Application

Syntax:    app(fn, arg)
Lean:      .app (fn : Expr) (arg : Expr)
Semantics: Opaque in eval (returns none). Compilation-only.
JS:        fn(arg)
Rust:      fn(arg)

Example: .app (.var "f") (.lit (.nat 5)) → JS: f(5) → Rust: f(5)

4.5 letE — Let Binding

Syntax:    let name = val in body
Lean:      .letE (name : String) (val : Expr) (body : Expr)
Semantics: Evaluate val, bind to name, evaluate body in extended env.
JS:        (() => { const name = val; return body; })()
Rust:      { let name = val; body }

Example: .letE "x" (.lit (.nat 5)) (.op .add [.var "x", .lit (.nat 3)]) → JS: (() => { const x = 5; return (x + 3); })() → eval: nat 8

Note: JS uses an IIFE wrapper because let in expressions requires a block. Rust uses a block expression naturally.

4.6 ifE — Conditional Expression

Syntax:    if cond then thn else els
Lean:      .ifE (cond : Expr) (thn : Expr) (els : Expr)
Semantics: Evaluate cond. If bool true, evaluate thn. If bool false, evaluate els.
           If cond is not a bool, returns none.
JS:        (cond ? thn : els)
Rust:      if cond { thn } else { els }

Example: .ifE (.lit (.bool true)) (.lit (.nat 1)) (.lit (.nat 2)) → JS: (true ? 1 : 2) → eval: nat 1

4.7 record — Record Construction

Syntax:    { k₁: e₁, ..., kₙ: eₙ }
Lean:      .record (fields : List (String × Expr))
Semantics: Evaluate each field value, return a record value.
JS:        { k₁: e₁, ..., kₙ: eₙ }
Rust:      json!({ "k₁": e₁, ..., "kₙ": eₙ })

Example: .record [("name", .lit (.str "alice")), ("age", .lit (.nat 30))] → JS: { name: 'alice', age: 30 } → Rust: json!({ "name": "alice".to_string(), "age": 30 })

4.8 get — Field Access

Syntax:    obj.field
Lean:      .get (obj : Expr) (field : String)
Semantics: If obj is a record, look up field by key. Returns none if missing.
JS:        obj.field        (if field is a name)
           obj[field]       (if field is all digits — array index)
Rust:      obj["field"]

Examples:

  • .get (.var "person") "name" → JS: person.name → Rust: person["name"]
  • .get (.var "pair") "0" → JS: pair[0] → Rust: pair["0"]

Numeric field detection: If field.all Char.isDigit is true, JS uses bracket notation [field] instead of dot notation .field. This handles tuple-like access patterns.

4.9 set — Record Update (Spread)

Syntax:    { obj with field = val }
Lean:      .set (obj : Expr) (field : String) (val : Expr)
Semantics: If obj is a record, update or insert the field. Returns a new record.
JS:        ({ ...obj, field: val })
Rust:      { let mut r = obj.clone(); r["field"] = val; r }

Example: .set (.var "config") "timeout" (.lit (.nat 5000)) → JS: ({ ...config, timeout: 5000 })

4.10 list — List Construction

Syntax:    [e₁, ..., eₙ]
Lean:      .list (elems : List Expr)
Semantics: Evaluate each element, return a list value.
JS:        [e₁, ..., eₙ]
Rust:      vec![e₁, ..., eₙ]

4.11 fold — Universal List Iteration

Syntax:    fold(lst, init, λ(acc, elem). body)
Lean:      .fold (lst : Expr) (init : Expr) (accParam : String) (elemParam : String) (body : Expr)
Semantics: If lst is a list, left-fold body over lst starting from init.
           For each element, bind accParam and elemParam, evaluate body.
           Returns final accumulator. Returns none if lst is not a list.
JS:        lst.reduce((accParam, elemParam) => body, init)
Rust:      lst.iter().fold(init, |accParam, elemParam| body)

Example — sum a list:

.fold
  (.list [.lit (.nat 1), .lit (.nat 2), .lit (.nat 3)])
  (.lit (.nat 0))
  "acc" "x"
  (.op .add [.var "acc", .var "x"])

→ JS: [1, 2, 3].reduce((acc, x) => (acc + x), 0) → eval: nat 6

Why fold? map, filter, find, any, all are all derivable from fold. One iteration primitive is sufficient. See §16 for derivations.

4.12 dispatch — Tag Dispatch

Syntax:    dispatch scrutinee { k₁ → e₁ | ... | kₙ → eₙ | default → e_d }
Lean:      .dispatch (scrutinee : Expr) (cases : List (String × Expr)) (default : Expr)
Semantics: Evaluate scrutinee. If it's a string, look up matching case.
           If found, evaluate that case's expression. Otherwise, evaluate default.
           If scrutinee is not a string, evaluate default.
JS:        (() => { switch (scrutinee) { case 'k₁': return e₁; ... default: return e_d; } })()
Rust:      match scrutinee.as_str() { "k₁" => e₁, ... _ => e_d }

Example — lifecycle state machine:

.dispatch (.var "state")
  [("Active", .lit (.bool true)),
   ("Paused", .op .eq [.var "eventType", .lit (.str "Resume")]),
   ("Terminated", .lit (.bool false))]
  (.lit (.bool false))

→ JS: (() => { switch (state) { case 'Active': return true; case 'Paused': return (eventType === 'Resume'); case 'Terminated': return false; default: return false; } })()

→ Rust: match state.as_str() { "Active" => true, "Paused" => (eventType == "Resume".to_string()), "Terminated" => false, _ => false }

Why dispatch and not match? This is tag dispatch on string values, not general pattern matching. String tags are the only scrutinee type. This matches the protocol's event-type dispatching pattern. General pattern matching is intentionally excluded — it would require a pattern language that adds complexity without protocol benefit.

4.13 op — Built-in Operation

Syntax:    op(args)
Lean:      .op (operation : Op) (args : List Expr)
Semantics: Evaluate all args, apply the built-in operation.
           See §6 for each operation's semantics.
JS/Rust:   See §6 for per-op compilation rules.

Example: .op .add [.lit (.nat 3), .lit (.nat 4)] → JS: (3 + 4) → eval: nat 7

4.14a prim — Typed Pure Primitive Call

Syntax:    prim(Prim.name, args)
Lean:      .prim (p : Prim) (args : List Expr)
Semantics: Opaque in eval (returns none). Trusted external function.
           See §7 for the full primitive registry.
JS:        primName(args)
Rust:      prim_name(args)

Example: .prim .sha256 [.var "data"] → JS: sha256(data) → Rust: sha256_hash(data)

4.14b call — String-based Call

Syntax:    call(name, args)
Lean:      .call (name : String) (args : List Expr)
Semantics: Opaque in eval (returns none). Dynamic dispatch by string name.
JS:        name(args)
Rust:      name(args)

Example: .call "new Uint8Array" [.lit (.nat 32)] → JS: new Uint8Array(32) → Rust: new Uint8Array(32)

When to use call vs prim: Use prim for the 30 enumerated trusted primitives. Use call for everything else: host APIs, method dispatch, constructor invocations. call is not in the TCB — it's string-based and the compiler passes it through verbatim. prim is in the TCB — the compiler knows every primitive by enum variant.

4.14c rawExpr — Escape Hatch

Syntax:    rawExpr(code)
Lean:      .rawExpr (code : String)
Semantics: Opaque in eval (returns none). Verbatim code injection.
JS:        code (unchanged)
Rust:      code with JS→Rust translations (0xFFn→0xFFu128, 1n→1u128)

See §9 for full details and audit.


5. Statement Grammar (7 forms)

Semantics: Stmt : Env × State → Value × OrderedEffectTrace

Statements extend expressions with ordered effects. State is external (host-persisted), read through Env, written via emit effects. Stmt execution does not mutate state in place — all state changes are effect descriptions.

Effects are ordered: emit A; emit B ≠ emit B; emit A

5.1 ret — Return Value

Syntax:    return val
Lean:      .ret (val : Expr)
Semantics: Return the evaluated expression. End the statement.
JS:        return val       (if val is not lit none)
           (empty string)   (if val is lit none — used in constructors)
Rust:      return val

Special case: .ret (.lit .none) compiles to empty string in JS. This is used in constructors where return null would be incorrect.

5.2 emit — Effect Emission

Syntax:    emit(type, { k₁: e₁, ... }); rest
Lean:      .emit (effectType : String) (payload : List (String × Expr)) (rest : Stmt)
Semantics: Push an effect record onto the trace, then continue with rest.
JS:        effects.push({ type: 'effectType', k₁: e₁, ... })
           rest
Rust:      effects.push(Effect { type_: "effectType", ... });
           rest

The effects array is implicitly created by EffectFuncDecl compilation. Every effect function starts with const effects = [].

Example:

.emit "storage.appendEvent"
  [("event", .var "signedEvent")]
  (.emit "transport.broadcast"
    [("filter", .lit (.str "*")), ("payload", .var "signedEvent")]
    (.ret (.var "signedEvent")))

→ JS:

effects.push({ type: 'storage.appendEvent', event: signedEvent })
effects.push({ type: 'transport.broadcast', filter: '*', payload: signedEvent })
return signedEvent

5.3 letS — Statement Binding

Syntax:    const name = val; body
Lean:      .letS (name : String) (val : Expr) (body : Stmt)
Semantics: Evaluate val, bind to name, continue with body.
JS:        const name = val
           body
           (if name starts with "_": val  (no const — void expression))
Rust:      let name = val;
           body

Underscore convention: If name starts with _, the binding is compiled as a void expression (no const keyword). This handles side-effectful expressions where the return value is discarded (e.g., buf.set(...) calls).

Example: .letS "_" (.call "buf.set" [.var "data", .lit (.nat 0)]) (.ret (.var "buf")) → JS: buf.set(data, 0)\n return buf

5.4 ifS — Statement Conditional

Syntax:    if cond then thn else els
Lean:      .ifS (cond : Expr) (thn : Stmt) (els : Stmt)
Semantics: Evaluate cond. Branch to thn or els.
JS:        if (cond) {
             thn
           } else {
             els
           }
Rust:      if cond {
             thn
           } else {
             els
           }

5.5 dispatch — Statement Tag Dispatch

Syntax:    switch scrutinee { k₁ → s₁ | ... | kₙ → sₙ | default → s_d }
Lean:      .dispatch (scrutinee : Expr) (cases : List (String × Stmt)) (default : Stmt)
JS:        switch (scrutinee) {
             case 'k₁':
               s₁
               break
             ...
             default:
               s_d
           }

5.6 forIn — Collection Iteration

Syntax:    for (elemVar of collection) { body }; rest
Lean:      .forIn (collection : Expr) (elemVar : String) (body : Stmt) (rest : Stmt)
JS:        for (const elemVar of collection) {
             body
           }
           rest

5.7 seq — Statement Sequencing

Syntax:    s₁; s₂
Lean:      .seq (a : Stmt) (b : Stmt)
JS:        a
           b

6. Built-in Operations (21 ops)

No overloading. Each op has exactly one domain. Where JavaScript uses + for both numbers and strings, EncDSL has separate add (numbers) and concatStr (strings).

Lean definition

inductive Op where
  | add | sub | mul | div | mod         -- Arithmetic (5)
  | band | bor | bxor | bnot | shl | shr  -- Bitwise (6)
  | eq | lt | le                        -- Comparison (3)
  | and | or | not                      -- Boolean (3)
  | concatStr | concatList              -- Concatenation (2)
  | lengthStr | lengthList | lengthBytes   -- Length (3)

Complete operation reference

Arithmetic (5)

OpArityDomainSemanticsJSRust
add2Nat × Nat → Nata + b(a + b)(a + b)
sub2Nat × Nat → Nata - b (truncating)(a - b)(a - b)
mul2Nat × Nat → Nata * b(a * b)(a * b)
div2Nat × Nat → Nata / b (floor division). Returns none if b = 0.(a / b)(a / b)
mod2Nat × Nat → Nata % b. Returns none if b = 0.(a % b)(a % b)

Bitwise (6)

OpArityDomainSemanticsJSRust
band2Nat × Nat → NatBitwise AND(a & b)(a & b)
bor2Nat × Nat → NatBitwise OR(a | b)(a | b)
bxor2Nat × Nat → NatBitwise XOR(a ^ b)(a ^ b)
bnot1Nat → NatBitwise NOT (opaque in eval — two's complement is platform-specific)~(a)(!a)
shl2Nat × Nat → NatLeft shift(a << b)(a << b)
shr2Nat × Nat → NatRight shift(a >> b)(a >> b)

RBAC bitmask usage: Bitwise ops are used extensively for the protocol's RBAC bitmask system. State bits occupy the low 8 bits (0xFF mask), trait bits start at bit 8. Example: getState(bitmask) = band(bitmask, 0xFF), hasTrait(bitmask, bit) = not(eq(band(shr(bitmask, bit), 1), 0)).

Comparison (3)

OpArityDomainSemanticsJSRust
eq2Val × Val → BoolStructural equality across all Val types(a === b)(a == b)
lt2Nat × Nat → BoolLess than(a < b)(a < b)
le2Nat × Nat → BoolLess than or equal(a <= b)(a <= b)

Note: eq uses strict equality (===) in JS. It works on all value types including strings. lt and le are defined only on naturals.

Boolean (3)

OpArityDomainSemanticsJSRust
and2Bool × Bool → BoolLogical AND(a && b)(a && b)
or2Bool × Bool → BoolLogical OR(a || b)(a || b)
not1Bool → BoolLogical NOT!(a)(!a)

Concatenation (2) — Disambiguated

OpArityDomainSemanticsJSRust
concatStr2Str × Str → StrString concatenation(a + b)format!("{}{}", a, b)
concatList2List × List → ListList concatenation[...a, ...b]{ let mut v = a.clone(); v.extend(b.iter()); v }

Why disambiguated? JS uses + for both. The DSL uses distinct ops so the type checker can verify no accidental string/list confusion. Both concatStr and concatList compile to (a + b) in JS, but concatList uses spread syntax and concatStr uses format! in Rust.

Length (3) — Disambiguated

OpArityDomainSemanticsJSRust
lengthStr1Str → NatString lengtha.lengtha.len()
lengthList1List → NatList lengtha.lengtha.len()
lengthBytes1Bytes → NatByte array lengtha.lengtha.len()

All three compile to .length in JS and .len() in Rust. They are distinct in the DSL for type-level reasoning.

Type errors in ops

If an op receives arguments of the wrong type (e.g., add on strings), evalOp returns none. The compilers do not insert runtime type checks — correctness is guaranteed by construction at the DSL level and verified by the Lean type checker.


7. Typed Primitive Registry (30 primitives)

The Prim type enumerates every trusted external function. Each is referentially transparent — same inputs, same outputs, no side effects. This set IS the trusted primitive surface.

Lean definition

inductive Prim where
  -- Crypto hashing (5)
  | sha256 | sha256Hash | sha256Str | taggedHash | buildSTHMessage
  -- Schnorr signing (3)
  | schnorrSign | schnorrVerify | derivePublicKey
  -- ECDH + encryption (4)
  | ecdh | deriveKey | encrypt | decrypt
  -- STH (2)
  | signSTH | verifySTH
  -- Protocol hashing (7)
  | computeContentHash | computeCommitHash | computeEventHash
  | computeEnclaveId | computeEventId | computeEventsRoot | encodeTags
  -- Domain hashing (4)
  | smtLeafHash | smtNodeHash | ctLeafHash | ctNodeHash
  -- Encoding (3)
  | bytesToHex | hexToBytes | jsonStringify
  -- Type coercion (2)
  | bigInt | number

Complete primitive reference

Crypto Hashing (5)

PrimSignaturePurposeJS NameRust Name
sha256bytes → bytesSHA-256 hashsha256sha256_hash
sha256Hashbytes → bytesSHA-256 hash (alias)sha256Hashsha256_hash
sha256Strstring → bytesSHA-256 of UTF-8 encoded stringsha256Strsha256_str
taggedHash(string, bytes) → bytesBIP-340 tagged hashtaggedHashtagged_hash
buildSTHMessage(nat, nat, bytes, bytes) → stringBuild signed tree head messagebuildSTHMessagebuild_sth_message

Schnorr Signing (3)

PrimSignaturePurposeJS NameRust Name
schnorrSign(bytes, bytes) → bytesBIP-340 Schnorr sign (zero aux bytes)schnorrSignschnorr_sign
schnorrVerify(bytes, bytes, bytes) → boolBIP-340 Schnorr verifyschnorrVerifyschnorr_verify
derivePublicKeybytes → bytesDerive secp256k1 x-only public keyderivePublicKeyderive_public_key

Determinism note: schnorrSign uses zero auxiliary bytes (new Uint8Array(32)) to ensure deterministic signatures. Standard BIP-340 uses random aux bytes for side-channel resistance, but the protocol requires cross-implementation reproducibility.

ECDH + Encryption (4)

PrimSignaturePurposeJS NameRust Name
ecdh(bytes, bytes) → bytesElliptic curve Diffie-Hellmanecdhecdh
deriveKey(bytes, string) → bytesKey derivation from shared secretderiveKeyderive_key
encrypt(bytes, bytes) → bytesXChaCha20-Poly1305 encryptencryptencrypt_
decrypt(bytes, bytes) → bytesXChaCha20-Poly1305 decryptdecryptdecrypt_

STH — Signed Tree Head (2)

PrimSignaturePurposeJS NameRust Name
signSTH(nat, nat, bytes, bytes) → stringSign a tree headsignSTHsign_sth
verifySTH(string, bytes, nat, nat, bytes) → boolVerify a tree head signatureverifySTHverify_sth

Protocol Hashing (7)

PrimSignaturePurposeJS NameRust Name
computeContentHashrecord → bytesHash event contentcomputeContentHashcompute_content_hash
computeCommitHashrecord → bytesHash commit structurecomputeCommitHashcompute_commit_hash
computeEventHashrecord → bytesHash full eventcomputeEventHashcompute_event_hash
computeEnclaveIdrecord → stringDerive enclave identifiercomputeEnclaveIdcompute_enclave_id
computeEventId(string, bytes, nat) → stringDerive event identifiercomputeEventIdcompute_event_id
computeEventsRootlist → bytesMerkle root of event listcomputeEventsRootcompute_events_root
encodeTagsrecord → stringEncode RBAC tagsencodeTagsencode_tags

Domain Hashing (4)

PrimSignaturePurposeJS NameRust Name
smtLeafHash(bytes, bytes) → bytesSMT leaf hash with domain prefix 0x20smtLeafHashsmt_leaf_hash
smtNodeHash(bytes, bytes) → bytesSMT node hash with domain prefix 0x21smtNodeHashsmt_node_hash
ctLeafHash(bytes, bytes) → bytesCT leaf hash with domain prefix 0x00ctLeafHashct_leaf_hash
ctNodeHash(bytes, bytes) → bytesCT node hash with domain prefix 0x01ctNodeHashct_node_hash

Domain separation: Each tree type (SMT, CT) uses a distinct domain prefix byte. This prevents second-preimage attacks where an attacker substitutes a leaf for a node. The prefixes are:

  • DOMAIN_CT_LEAF = 0x00
  • DOMAIN_CT_NODE = 0x01
  • DOMAIN_COMMIT = 0x10
  • DOMAIN_EVENT = 0x11
  • DOMAIN_ENCLAVE = 0x12
  • DOMAIN_SMT_LEAF = 0x20
  • DOMAIN_SMT_NODE = 0x21

Encoding (3)

PrimSignaturePurposeJS NameRust Name
bytesToHexbytes → stringHex-encode bytesbytesToHexbytes_to_hex
hexToBytesstring → bytesHex-decode stringhexToByteshex_to_bytes
jsonStringifyval → stringJSON serializationJSON.stringifyserde_json::to_string

Type Coercion (2)

PrimSignaturePurposeJS NameRust Name
bigIntval → BigIntConvert to BigInt (JS-specific)BigInt(no-op)
numberval → NumberConvert to Number (JS-specific)Number(no-op)

Cross-backend note: bigInt and number are no-ops in Rust because Rust uses fixed-width integer types. In JS, they are required for bitmask operations (which use BigInt) and for converting back to Number for array indexing.

In the Rust compiler, if the primitive name resolves to empty string, the argument is passed through unchanged:

| .bigInt => "" | .number => ""  -- no-op in Rust
if rsName == "" then
  if args.length == 1 then argStr else s!"/* coerce */ {argStr}"

8. String-based Calls

| call (name : String) (args : List Expr)

call is the general-purpose function invocation form. Unlike prim, the function name is a raw string — the compiler passes it through verbatim.

When to use call

  • Host API methods: .call "new Uint8Array" [.lit (.nat 32)]
  • Method dispatch: .call "buf.set" [.var "data", .lit (.nat 0)]
  • Constructor invocations: .call "new TextEncoder().encode" [.var "str"]
  • Object methods: .call "Object.freeze" [.var "obj"]
  • Low-level crypto calls: .call "schnorr.sign" [.var "msg", .var "key", .var "auxRand"] (when the wrapper body needs to call the underlying noble import)

Trust implications

call is not in the TCB. It's string-based and the compiler does not verify that the named function exists or has the correct signature. Trust comes from:

  1. The Lean type checker verifying the DSL program is well-formed
  2. The test suite verifying the generated code is correct
  3. The refinement proofs verifying that eval and compile agree

9. Raw Expression Escape Hatch

| rawExpr (code : String)

rawExpr injects verbatim code into the output. This is in the TCB. Every rawExpr is quarantined and the goal is elimination.

JS compilation

The code string is emitted unchanged: compileExpr (.rawExpr code) = code

Rust compilation

JS-specific patterns are translated:

  • 0xFFn0xFFu128
  • 0n0u128
  • 1n1u128

Current usage (audit)

PatternCountExampleWhy not prim/call?
BigInt literals~5rawExpr "0xFFn"JS BigInt syntax, no function call
BigInt arithmetic~3rawExpr "1n"Literal, not a function
Property access~10rawExpr "this.leaves.length"Property, not a function call
Void IIFEs~5rawExpr "(() => { ... })()"Complex initialization blocks

Goal

Eliminate all rawExpr usage. Each can be replaced by:

  • BigInt literals → prim .bigInt [lit n]
  • Property access → get (var "this") "leaves" chained
  • Void IIFEs → restructure as letS sequences

10. Module IR

The module IR is backend organizational structure, not part of the semantic core. It wraps expressions and statements into files.

FuncDecl — Pure Function

structure FuncDecl where
  name : String          -- function name
  params : List String   -- parameter names
  isExport : Bool := true
  isAsync : Bool := false
  body : Expr            -- expression body (pure)
Compilation (JS):
export function name(params) {
  return body
}

EffectFuncDecl — Effect Function

structure EffectFuncDecl where
  name : String
  params : List String
  isExport : Bool := true
  isAsync : Bool := false
  body : Stmt            -- statement body (can emit effects)
Compilation (JS):
export function name(params) {
  const effects = []
  body
}

The effects array is automatically created. Statement emit calls push to it.

MethodDecl — Class Method

structure MethodDecl where
  name : String
  params : List String
  isAsync : Bool := false
  body : Stmt

ClassDecl — Class

structure ClassDecl where
  name : String
  isExport : Bool := true
  constructorParams : List String := []
  constructorBody : Stmt := .ret (.lit .none)
  methods : List MethodDecl := []
Compilation (JS):
export class Name {
  constructor(params) {
    constructorBody
  }
 
  methodName(params) {
    methodBody
  }
}

ModuleDecl — File

structure ModuleDecl where
  name : String
  header : String
  imports : List (List String × String)   -- (names, source)
  constants : List (String × Expr) := []
  classes : List ClassDecl := []
  functions : List FuncDecl := []
  effectFunctions : List EffectFuncDecl := []
Compilation (JS):
/**
 * header
 */
 
import { names } from 'source'
 
export const NAME = expr
 
export function funcName(params) { ... }
 
export function effectFuncName(params) { ... }
 
export class ClassName { ... }

Sections are joined with \n\n. Empty sections are omitted.


11. Compilation Rules — JavaScript

Source: spec-lean/Enc/DSL/Compile.lean

compileExpr : Expr → String

FormOutput
var namename
lit (nat n)n
lit (bool true)true
lit (bool false)false
lit (str s)'s'
lit (bytes bs)new Uint8Array([b₁, b₂, ...])
lit (list vs)[v₁, v₂, ...]
lit (record fs){ k₁: v₁, ... }
lit nonenull
lam p body(p) => body
app fn argfn(arg)
letE n v body(() => { const n = v; return body; })()
ifE c t e(c ? t : e)
record fs{ k₁: e₁, ... }
get obj f (name)obj.f
get obj f (digits)obj[f]
set obj f v({ ...obj, f: v })
list es[e₁, ...]
fold l i ap ep bl.reduce((ap, ep) => b, i)
dispatch s cs d(() => { switch (s) { case 'k': return e; ... default: return d; } })()
op o args(see §6)
prim p argsprimJSName(args)
call n argsn(args)
rawExpr codecode

compileStmt : Stmt → String

FormOutput
ret (lit none)`` (empty)
ret valreturn val
emit type payload resteffects.push({ type: 'type', k: v, ... })\nrest
letS name val body (name starts with _)val\nbody
letS name val bodyconst name = val\nbody
ifS c t eif (c) {\n t\n} else {\n e\n}
dispatch s cs dswitch (s) {\n case 'k':\n body\n break\n ...\n default:\n d\n}
forIn coll v body restfor (const v of coll) {\n body\n}\nrest
seq a ba\nb

compileModule : ModuleDecl → String

Assembles header, imports, constants, functions, effect functions, and classes into a complete JavaScript file. Empty sections are filtered out. Sections are separated by \n\n.


12. Compilation Rules — Rust

Source: spec-lean/Enc/DSL/CompileRs.lean

Key differences from JS

ConstructJSRust
String literal'hello'"hello".to_string()
None/nullnullNone
Bytesnew Uint8Array([...])vec![...]
Record{ k: v }json!({ "k": v })
Record update{ ...obj, k: v }{ let mut r = obj.clone(); r["k"] = v; r }
Lambda(x) => body|x| body
Let expression(() => { const x = v; return body; })(){ let x = v; body }
If expression(c ? t : e)if c { t } else { e }
Dispatchswitch(s) { ... }match s.as_str() { ... }
Foldxs.reduce((a,x) => b, init)xs.iter().fold(init, |a, x| b)
List concat[...a, ...b]{ let mut v = a.clone(); v.extend(b.iter()); v }
String concat(a + b)format!("{}{}", a, b)
Equality=====
BigInt coercionBigInt(x) / Number(x)(no-op)

Prim → Rust naming convention

All Prim names are converted from camelCase to snake_case:

PrimRust function
sha256sha256_hash
sha256Hashsha256_hash
sha256Strsha256_str
schnorrSignschnorr_sign
schnorrVerifyschnorr_verify
derivePublicKeyderive_public_key
computeContentHashcompute_content_hash
bytesToHexbytes_to_hex
hexToByteshex_to_bytes
jsonStringifyserde_json::to_string
bigInt(no-op)
number(no-op)

rawExpr in Rust

JS-specific patterns are auto-translated:

  • 0xFFn0xFFu128 (BigInt → u128)
  • 0n0u128
  • 1n1u128

13. Evaluator Semantics

Source: spec-lean/Enc/DSL/Eval.lean

Environment

def Env := List (String × Val)
 
def Env.lookup (env : Env) (name : String) : Val
def Env.extend (env : Env) (name : String) (v : Val) : Env

The environment is an association list. lookup returns none for unbound variables. extend prepends a binding (shadowing earlier bindings of the same name).

eval : Env → Expr → Val

The evaluator is a partial function (Lean partial def) that maps expressions to values. It handles:

  • var → environment lookup
  • lit → identity
  • lam / app → opaque (returns none) — closures exist only at compilation level
  • letE → evaluate value, extend environment, evaluate body
  • ifE → evaluate condition, branch (must be bool)
  • record → evaluate all field values
  • get → find field in record
  • set → update or insert field in record
  • list → evaluate all elements
  • fold → left-fold over list elements
  • dispatch → match string tag against cases
  • op → delegate to evalOp
  • prim / call / rawExpr → opaque (returns none)

evalOp : Op → List Val → Val

Evaluates built-in operations on concrete values. Type mismatches return none.

Important semantic differences from backends:
  • div and mod return none on division by zero (JS returns Infinity / NaN)
  • bnot returns a unchanged (two's complement bitwise NOT is platform-specific; eval doesn't model it)
  • eq on different-type values: structural comparison via BEq

14. Effect System

Effect structure

Effects are records with a type field and arbitrary payload:

{ type: 'effectType', key₁: value₁, key₂: value₂, ... }

Effect types (exhaustive taxonomy)

Effect TypePayloadSemantics
storage.appendEvent{ event }Append event to persistent event log
storage.writeState{ state }Persist node state snapshot
storage.persistBundle{ bundle }Persist bundle metadata
transport.respond{ payload, status }Send HTTP response to caller
transport.broadcast{ filter, payload }Broadcast to matching WebSocket subscribers
notification.push{ endpoint, payload, mode }Enqueue push notification delivery
timer.schedule{ at, kind }Schedule alarm for future processing

Ordering invariant

Effects are emitted in program order. The host MUST interpret them in array order. Reordering is a protocol violation.

State interaction

Statements do not mutate state in place. All state changes flow through emit:

  1. Kernel computes new state from inputs
  2. Kernel emits storage.writeState effect with new state
  3. Host persists state after receiving the effect
  4. Next step() call receives the persisted state through its arguments

15. Trust Surface

ConstructPurposeTrusted?CountGoal
primPure external primitiveYes — TCB30Minimize
emitHost interactionInterpreted7 typesFormalize
rawExprBackend escape hatchYes — TCB~23Eliminate
callString-based dispatchNo — pass-throughunboundedN/A
opBuilt-in operationNo — Verified21Proven
var/lit/lam/...Core calculusNo — Verified11 formsProven

TCB (Trusted Computing Base)

What you must trust for correctness:

  1. Lean 4 kernel and standard axioms — verifies all proofs
  2. @noble cryptographic primitives — secp256k1, SHA-256, XChaCha20
  3. k256 crate (Rust) — BIP-340 Schnorr, validated via cross-impl tests
  4. 30 Prim implementations — each must be referentially transparent
  5. ~23 rawExpr blocks — verbatim code, goal is elimination
  6. Host runtime — Cloudflare Workers (V8), Node.js

What is NOT in the TCB

  • All 21 op operations — semantics proven in Lean
  • All 11 core expression forms — compilation proven correct
  • Module IR assembly — structural correctness verified
  • Effect ordering — guaranteed by statement semantics

16. Standard Library Derivations

These are NOT built-in. They derive from fold + lam:

-- map: transform each element
map(xs, f) = fold(xs, [], λ(acc, x). concatList(acc, [f(x)]))
 
-- filter: keep elements matching predicate
filter(xs, p) = fold(xs, [], λ(acc, x). if p(x) then concatList(acc, [x]) else acc)
 
-- find: first matching element
find(xs, p) = fold(xs, none, λ(acc, x). if acc != none then acc else if p(x) then x else none)
 
-- any: does any element match?
any(xs, p) = fold(xs, false, λ(acc, x). or(acc, p(x)))
 
-- all: do all elements match?
all(xs, p) = fold(xs, true, λ(acc, x). and(acc, p(x)))
 
-- sum: numeric reduction
sum(xs) = fold(xs, 0, λ(acc, x). add(acc, x))
 
-- count via fold
count(xs) = fold(xs, 0, λ(acc, _). add(acc, 1))

In practice, protocol modules use .fold directly rather than defining these as named functions. The derivations show that fold is the only iteration primitive needed.


17. Termination

All recursion is through fold over finite lists. No unbounded recursion. No fuel parameter.

For tree algorithms (SMT depth=168, CT depth=variable) that traverse by depth: the depth range is expressed as a fold over a list literal [0, 1, ..., DEPTH-1], or over a proof's sibling list. The accumulator carries the running hash.

Example — SMT verify (sketch):

.fold (.var "siblings")
  (.var "initialHash")
  "hash" "entry"
  (.letE "sibling" (.get (.var "entry") "hash")
    (.letE "direction" (.get (.var "entry") "direction")
      (.ifE (.op .eq [.var "direction", .lit (.str "left")])
        (.prim .smtNodeHash [.var "sibling", .var "hash"])
        (.prim .smtNodeHash [.var "hash", .var "sibling"]))))

No recursion — just a left-fold over a finite list. The list is bounded by tree depth (168 for SMT, log₂(n) for CT).


18. Proof Inventory

Source: spec-lean/Enc/DSL/Refinement.lean

55 refinement theorems, all kernel-checked via native_decide.

CategoryCountWhat's proven
RBAC eval7getState, hasTrait, isOutsider on concrete inputs
Lifecycle eval4State machine transitions (Active, Paused, Terminated)
Arithmetic eval6add, sub, mul, div, div_zero, mod
Bitwise eval5band, bor, bxor, shl, shr
Comparison + Boolean eval10eq, neq, lt, le, and, or, not
Binding + Record + Control5let, record.get, record.set, if true, if false
Fold eval2sum [1,2,3,4] = 10, length via fold = 3
Compilation13compileExpr produces expected JS strings
Cross-checks2eval and compileExpr agree on same input

Proof technique

All theorems use native_decide — Lean's kernel evaluates the expression and checks the result. This is sound because:

  1. The expression is fully concrete (no free variables)
  2. The evaluator is deterministic
  3. native_decide produces a kernel-checked proof certificate

DecidableEq caveat

Val uses a sorry bridge for DecidableEq (derived from BEq). This is a technical limitation — Lean can't auto-derive DecidableEq for recursive inductives. The sorry does not affect theorem soundness because all 55 theorems are computationally verified by native_decide, which checks the actual BEq comparison at runtime.


19. Generated File Inventory

Source: spec-lean/Enc/Gen.lean

19 files total. 11 from DSL v4, 8 from templates.

sdk/core/ — 6 files (DSL v4)

FileModuleFunctionsKey constructs
types.jsTypes.leanConstants onlyObject.freeze, enum records
crypto.jsCrypto.lean15 functionsDomain hashes, Schnorr wrappers
rbac.jsRbac.lean + RbacEffect.lean12 functionsBitmask ops, isAuthorized
event.jsEvent.lean8 functionsmkCommit, signCommit, verifyCommit
smt.jsSmt.lean + SmtClass.leanSparseMerkleTree classinsert, remove, verify
ct.jsCt.leanCertificateTransparency classappend, inclusionProof, consistencyProof

js/node/ — 5 files (DSL v4)

FileModuleKey constructs
node.jsNodeClass.leanNode class — commit processing pipeline
handlers.jsHandlers.leanREST handlers — handleCommit, handleQuery
persistence.jsPersistence.leanState persistence — saveState, loadState
push.jsPush.leanPush notifications — deliverPush
ws-handlers.jsWsHandlers.leanWebSocket — handleWsMessage, broadcast

js/node/worker/ — 2 files (Templates)

FileSourcePurpose
enclave.jsBodies/Enclave.leanCloudflare Durable Object
index.jsBodies/WorkerIndex.leanCloudflare Worker entry point

sdk/client/ — 6 files (Templates)

FileSourcePurpose
http.jsBodies/Http.leanHTTP client
sdk.jsBodies/Sdk.leanSDK class
session.jsBodies/Session.leanSession management
ws.jsBodies/Ws.leanWebSocket client
registry-client.jsBodies/RegistryClient.leanRegistry lookup
wallet.jsBodies/Wallet.leanWallet operations

Build command

cd spec/spec-lean
lake build                          # Type-check all proofs (259 theorems)
lake env lean --run Enc/Gen.lean    # Generate 19 files

Or via package.json:

yarn build:js    # Runs both commands

20. Worked Examples

Example 1: RBAC getState

Extract the state byte (low 8 bits) from a bitmask.

-- DSL
def getState : Expr := .op .band [.var "bitmask", .rawExpr "0xFFn"]

Eval (on concrete input):

eval [] (.op .band [.lit (.nat 0x1FF), .lit (.nat 0xFF)]) = .nat 0xFF

JS output: (bitmask & 0xFFn)

Rust output: (bitmask & 0xFFu128)

Example 2: Domain hash function

SHA-256 with a domain prefix byte.

def domainHash : Expr :=
  .letE "buf" (.call "new Uint8Array" [.op .add [.lit (.nat 1), .get (.var "data") "length"]])
    (.letE "_" (.call "buf.set" [.list [.var "prefix"], .lit (.nat 0)])
      (.letE "_" (.call "buf.set" [.var "data", .lit (.nat 1)])
        (.prim .sha256 [.var "buf"])))
JS output:
(() => { const buf = new Uint8Array((1 + data.length)); buf.set([prefix], 0); buf.set(data, 1); return sha256(buf); })()

Constructs used: letE, call, op .add, get, list, prim .sha256, underscore binding convention

Example 3: Lifecycle state machine

Dispatch on enclave lifecycle state.

def isActive : Expr :=
  .dispatch (.var "state")
    [("Active", .lit (.bool true)),
     ("Paused", .op .eq [.var "eventType", .lit (.str "Resume")]),
     ("Terminated", .lit (.bool false))]
    (.lit (.bool false))
JS output:
(() => { switch (state) { case 'Active': return true; case 'Paused': return (eventType === 'Resume'); case 'Terminated': return false; default: return false; } })()
Proven properties:
theorem lifecycle_active : eval [] (lifecycle_test "Active" "post") = .bool true
theorem lifecycle_paused_resume : eval [] (lifecycle_test "Paused" "Resume") = .bool true
theorem lifecycle_paused_post : eval [] (lifecycle_test "Paused" "post") = .bool false
theorem lifecycle_terminated : eval [] (lifecycle_test "Terminated" "Resume") = .bool false

Example 4: Effect function — commit processing

def handleCommit : EffectFuncDecl := {
  name := "handleCommit"
  params := ["state", "commit", "context"]
  body :=
    .letS "result" (.call "processCommit" [.var "state", .var "commit", .var "context"])
      (.emit "storage.appendEvent" [("event", .get (.var "result") "event")]
        (.emit "storage.writeState" [("state", .get (.var "result") "newState")]
          (.emit "transport.broadcast" [("filter", .lit (.str "*")), ("payload", .get (.var "result") "event")]
            (.ret (.get (.var "result") "event")))))
}
JS output:
export function handleCommit(state, commit, context) {
  const effects = []
  const result = processCommit(state, commit, context)
  effects.push({ type: 'storage.appendEvent', event: result.event })
  effects.push({ type: 'storage.writeState', state: result.newState })
  effects.push({ type: 'transport.broadcast', filter: '*', payload: result.event })
  return result.event
}

Example 5: Fold — SMT verification

def verifyProof : Expr :=
  .fold (.var "siblings")
    (.var "leafHash")
    "currentHash" "sibling"
    (.ifE (.get (.var "sibling") "isLeft")
      (.prim .smtNodeHash [.get (.var "sibling") "hash", .var "currentHash"])
      (.prim .smtNodeHash [.var "currentHash", .get (.var "sibling") "hash"]))
JS output:
siblings.reduce((currentHash, sibling) => (sibling.isLeft ? smtNodeHash(sibling.hash, currentHash) : smtNodeHash(currentHash, sibling.hash)), leafHash)

Appendix A: File Map

spec-lean/Enc/DSL/
  Grammar.lean        14 Expr + 21 Op + 7 Stmt + 30 Prim + Module IR
  Compile.lean        compileExpr, compileStmt, compileModule → JS
  CompileRs.lean      compileExprRs, compileOpRs → Rust
  Eval.lean           eval : Env → Expr → Val
  Refinement.lean     55 theorems (native_decide)
  Gen.lean            Module assembly
  Modules/            14 protocol module definitions
    Types.lean        Protocol enums and constants
    Crypto.lean       Domain hashes, Schnorr wrappers
    Rbac.lean         Bitmask operations (pure expressions)
    RbacEffect.lean   RBAC with effects (statements)
    Smt.lean          Sparse Merkle Tree functions
    SmtClass.lean     SparseMerkleTree class
    Ct.lean           Certificate Transparency
    Event.lean        Commit, event, receipt
    NodeClass.lean    Node class
    Handlers.lean     REST handlers
    Persistence.lean  State persistence
    Push.lean         Push notifications
    WsHandlers.lean   WebSocket handlers
    Node.lean         Node module assembly

Appendix B: Quick Reference Card

Expr (14 forms):
  var name | lit val | lam p body | app fn arg | letE n v body
  ifE c t e | record [(k,e)] | get obj f | set obj f v | list [e]
  fold lst init acc elem body | dispatch scrut [(k,e)] default
  op Op [args] | prim Prim [args] | call name [args] | rawExpr code
 
Stmt (7 forms):
  ret val | emit type [(k,e)] rest | letS n v body
  ifS c t e | dispatch scrut [(k,s)] default
  forIn coll var body rest | seq a b
 
Op (21):
  add sub mul div mod                    -- arithmetic
  band bor bxor bnot shl shr            -- bitwise
  eq lt le                               -- comparison
  and or not                             -- boolean
  concatStr concatList                   -- concatenation
  lengthStr lengthList lengthBytes       -- length
 
Prim (30):
  sha256 sha256Hash sha256Str taggedHash buildSTHMessage
  schnorrSign schnorrVerify derivePublicKey
  ecdh deriveKey encrypt decrypt
  signSTH verifySTH
  computeContentHash computeCommitHash computeEventHash
  computeEnclaveId computeEventId computeEventsRoot encodeTags
  smtLeafHash smtNodeHash ctLeafHash ctNodeHash
  bytesToHex hexToBytes jsonStringify
  bigInt number
 
Val (7 types):
  nat | bool | str | bytes | list | record | none