Formally verifying Aiur programs #340

Description

@arthurpaulino

Formal Evaluation Semantics for Aiur

Goal

Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
Terms — so that we can state and prove theorems about the behavior of Aiur programs.
This is not meant to be efficient; it is a specification against which the bytecode
compiler and the Rust runtime can be verified.

Overview

The core idea: given a Toplevel (the set of all function/datatype declarations) and
a Function, evaluating that function on concrete Value arguments produces a
Result containing:

  • An output Value (the return value).
  • A Prop-valued side condition accumulating all assertEq obligations.

This lets us state theorems like:

-- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
theorem foo_spec (top : Toplevel) (x : G) :
Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)

Core Definitions

Values

Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
is fully reduced:

inductiveValuewhere
| unit : Value
| field : G → Value
| tuple : Array Value → Value
| array : Array Value → Value
deriving Repr, BEq, Inhabited

Memory

Store/load operations require a heap model. A Memory is a simple map from addresses
to values:

structureMemorywherecells : Array Value
deriving Inhabited
defMemory.store (mem : Memory) (v : Value) : G × Memory :=
let addr := G.ofNat mem.cells.size
(addr, { cells := mem.cells.push v })
defMemory.load (mem : Memory) (addr : G) : Option Value :=
mem.cells[addr.val.toNat]?

Result and EvalState

Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
and stateful (memory, IO buffers). We track:

structureEvalStatewherememory : Memory
ioBuffer : IOBuffer
fuel : Nat -- recursion boundinductiveResultwhere
| ok : Value → Prop → Result -- value + accumulated assertions
| fail : String → Result -- stuck / out of fuel / match failure

The Prop component in ok is the conjunction of all assertEq conditions
encountered during evaluation. When there are no assertions, this is True.

Environment

An Env maps local variables to values. Function lookup goes through the Toplevel:

abbrev Env := Std.HashMap Local Value
deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
top.functions.find? fun f => f.name == name

The Evaluator

partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
Term → EvalState × Result

The key cases:

TermSemantics
.var xLook up x in env
.data (.field g).ok (.field g) True
.data (.tuple ts)Evaluate each element, combine
.data (.array ts)Evaluate each element, combine
.ret tEvaluate t (marks escaping to caller)
.let pat val bodyEvaluate val, match against pat to extend env, evaluate body
.match t branchesEvaluate t, try each (pat, body) in order
.app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
.add a bEvaluate both to fields, return field addition (mod gSize)
.sub a bField subtraction
.mul a bField multiplication
.eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
.assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
.store tEvaluate t, push to memory, return pointer field
.load tEvaluate t to a field (address), look up in memory
.proj t iEvaluate t to a tuple, project component i
.get t iEvaluate t to an array, index at i
.slice t i jEvaluate t to an array, extract [i, j)
.set t i vEvaluate t to an array, evaluate v, replace at i

For function calls, fuel is decremented. If fuel reaches 0, the result is
.fail "out of fuel". This makes eval total.

Pattern Matching

defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

This attempts to destructure v according to pat, returning bindings on success.
For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
recursively matches sub-patterns.

assertEq Accumulation

The crucial design point: assertEq does not fail evaluation. Instead, the equality
is accumulated as a Prop in the result:

| .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
let (state, b_val, b_prop) ← evalValue top env state b
let (state, ret_val, ret_prop) ← eval top env state ret
let assertion := (a_val = b_val)
(state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
The evaluator produces the output regardless, and the proposition captures what must
hold for the execution to be valid.

Flattening to Array G

To connect eval to the existing test infrastructure (which works with Array G),
define:

defValue.flatten : Value → Array G
| .unit => #[]
| .field g => #[g]
| .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
| .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

And a top-level entry point:

defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
(fuel : Nat := 1000) : Option (Array G × Prop) :=
let func ← lookupFunction top funcName
let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
let state := { memory := default, ioBuffer := default, fuel }
match eval top env state func.body with
| (_, .ok val prop) => some (val.flatten, prop)
| _ => none

Stating Theorems

With this setup, we can write specifications like:

-- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
(h_func : lookupFunction top `foo = some foo_fn) :
Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
...
-- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
(h_func : lookupFunction top `sum = some sum_fn) :
Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
...
-- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
Aiur.run top `abs_or_zero #[x] =
if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
...

Key Design Decisions

1. Prop-valued assertions, not runtime failure

assertEq accumulates a Prop rather than causing eval to fail. This is essential
because Aiur is a circuit language: the prover always produces an output, and the
constraints determine whether the output is valid. Separating the output from the
validity condition is what makes formal verification useful — you can reason about what
the circuit computes independently of whether the constraints are satisfied.

2. Fuel-based termination

Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
bounded recursion.

3. Operating on Term (pre-simplification)

eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
means it can be used to specify the semantics of Aiur programs as written. A separate
correctness theorem would relate eval on Term to execution of compiled Bytecode.

4. Memory as an array

The memory model is a simple append-only array. store appends and returns the index
as a G. load indexes into the array. This is adequate because Aiur memory is
append-only (no mutation of existing cells).

Verification Roadmap

Phase 1: Definitional interpreter (Aiur.eval)

  • Define Value, Memory, EvalState, Result, Env.
  • Implement eval, matchPattern, Value.flatten.
  • Implement Aiur.run as the top-level entry point.

Phase 2: Basic specifications

  • State and prove specs for simple functions: id, sum, prod.
  • State and prove specs involving assertEq.
  • State and prove specs involving match on field values.

Phase 3: Data structure operations

  • Specs for proj, get, slice, set.
  • Specs for tuple/array construction and destructuring.
  • Specs involving store/load.

Phase 4: Recursion and datatypes

  • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
  • Specs for enum construction and pattern matching (Shape, Nat).
  • Mutual recursion (even/odd).

Phase 5: Compiler correctness

  • Define a relation between eval on Term and Bytecode.Toplevel.execute.
  • Prove that compilation preserves semantics: if eval produces (val, prop),
    then execute on the compiled bytecode produces val.flatten (assuming prop
    holds).

Phase 6: Byte and u32 operations

  • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
  • Specs for u32_less_than.
  • These require modeling the Goldilocks field arithmetic precisely.

Relation to CSLib

The approach is analogous to how CSLib provides
foundational definitions for formalizing computer science in Lean. Where CSLib defines
evaluation semantics for lambda calculus and other foundational languages, we define
evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
is: define a clean denotational/operational semantics first, then prove properties
against it, then (optionally) prove that concrete implementations refine the semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      Skip to content

      Formally verifying Aiur programs #340

      Description

      @arthurpaulino

      Formal Evaluation Semantics for Aiur

      Goal

      Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
      Terms — so that we can state and prove theorems about the behavior of Aiur programs.
      This is not meant to be efficient; it is a specification against which the bytecode
      compiler and the Rust runtime can be verified.

      Overview

      The core idea: given a Toplevel (the set of all function/datatype declarations) and
      a Function, evaluating that function on concrete Value arguments produces a
      Result containing:

      • An output Value (the return value).
      • A Prop-valued side condition accumulating all assertEq obligations.

      This lets us state theorems like:

      -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
      theorem foo_spec (top : Toplevel) (x : G) :
      Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
      

      Core Definitions

      Values

      Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
      is fully reduced:

      inductiveValuewhere
      | unit : Value
      | field : G → Value
      | tuple : Array Value → Value
      | array : Array Value → Value
      deriving Repr, BEq, Inhabited

      Memory

      Store/load operations require a heap model. A Memory is a simple map from addresses
      to values:

      structureMemorywherecells : Array Value
      deriving Inhabited
      defMemory.store (mem : Memory) (v : Value) : G × Memory :=
      let addr := G.ofNat mem.cells.size
      (addr, { cells := mem.cells.push v })
      defMemory.load (mem : Memory) (addr : G) : Option Value :=
      mem.cells[addr.val.toNat]?

      Result and EvalState

      Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
      and stateful (memory, IO buffers). We track:

      structureEvalStatewherememory : Memory
      ioBuffer : IOBuffer
      fuel : Nat -- recursion boundinductiveResultwhere
      | ok : Value → Prop → Result -- value + accumulated assertions
      | fail : String → Result -- stuck / out of fuel / match failure

      The Prop component in ok is the conjunction of all assertEq conditions
      encountered during evaluation. When there are no assertions, this is True.

      Environment

      An Env maps local variables to values. Function lookup goes through the Toplevel:

      abbrev Env := Std.HashMap Local Value
      deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
      top.functions.find? fun f => f.name == name

      The Evaluator

      partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
      Term → EvalState × Result

      The key cases:

      TermSemantics
      .var xLook up x in env
      .data (.field g).ok (.field g) True
      .data (.tuple ts)Evaluate each element, combine
      .data (.array ts)Evaluate each element, combine
      .ret tEvaluate t (marks escaping to caller)
      .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
      .match t branchesEvaluate t, try each (pat, body) in order
      .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
      .add a bEvaluate both to fields, return field addition (mod gSize)
      .sub a bField subtraction
      .mul a bField multiplication
      .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
      .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
      .store tEvaluate t, push to memory, return pointer field
      .load tEvaluate t to a field (address), look up in memory
      .proj t iEvaluate t to a tuple, project component i
      .get t iEvaluate t to an array, index at i
      .slice t i jEvaluate t to an array, extract [i, j)
      .set t i vEvaluate t to an array, evaluate v, replace at i

      For function calls, fuel is decremented. If fuel reaches 0, the result is
      .fail "out of fuel". This makes eval total.

      Pattern Matching

      defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

      This attempts to destructure v according to pat, returning bindings on success.
      For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
      succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
      recursively matches sub-patterns.

      assertEq Accumulation

      The crucial design point: assertEq does not fail evaluation. Instead, the equality
      is accumulated as a Prop in the result:

      | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
      let (state, b_val, b_prop) ← evalValue top env state b
      let (state, ret_val, ret_prop) ← eval top env state ret
      let assertion := (a_val = b_val)
      (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

      This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
      The evaluator produces the output regardless, and the proposition captures what must
      hold for the execution to be valid.

      Flattening to Array G

      To connect eval to the existing test infrastructure (which works with Array G),
      define:

      defValue.flatten : Value → Array G
      | .unit => #[]
      | .field g => #[g]
      | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
      | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

      And a top-level entry point:

      defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
      (fuel : Nat := 1000) : Option (Array G × Prop) :=
      let func ← lookupFunction top funcName
      let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
      let state := { memory := default, ioBuffer := default, fuel }
      match eval top env state func.body with
      | (_, .ok val prop) => some (val.flatten, prop)
      | _ => none

      Stating Theorems

      With this setup, we can write specifications like:

      -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
      (h_func : lookupFunction top `foo = some foo_fn) :
      Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
      ...
      -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
      (h_func : lookupFunction top `sum = some sum_fn) :
      Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
      ...
      -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
      Aiur.run top `abs_or_zero #[x] =
      if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
      ...

      Key Design Decisions

      1. Prop-valued assertions, not runtime failure

      assertEq accumulates a Prop rather than causing eval to fail. This is essential
      because Aiur is a circuit language: the prover always produces an output, and the
      constraints determine whether the output is valid. Separating the output from the
      validity condition is what makes formal verification useful — you can reason about what
      the circuit computes independently of whether the constraints are satisfied.

      2. Fuel-based termination

      Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
      are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
      bounded recursion.

      3. Operating on Term (pre-simplification)

      eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
      means it can be used to specify the semantics of Aiur programs as written. A separate
      correctness theorem would relate eval on Term to execution of compiled Bytecode.

      4. Memory as an array

      The memory model is a simple append-only array. store appends and returns the index
      as a G. load indexes into the array. This is adequate because Aiur memory is
      append-only (no mutation of existing cells).

      Verification Roadmap

      Phase 1: Definitional interpreter (Aiur.eval)

      • Define Value, Memory, EvalState, Result, Env.
      • Implement eval, matchPattern, Value.flatten.
      • Implement Aiur.run as the top-level entry point.

      Phase 2: Basic specifications

      • State and prove specs for simple functions: id, sum, prod.
      • State and prove specs involving assertEq.
      • State and prove specs involving match on field values.

      Phase 3: Data structure operations

      • Specs for proj, get, slice, set.
      • Specs for tuple/array construction and destructuring.
      • Specs involving store/load.

      Phase 4: Recursion and datatypes

      • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
      • Specs for enum construction and pattern matching (Shape, Nat).
      • Mutual recursion (even/odd).

      Phase 5: Compiler correctness

      • Define a relation between eval on Term and Bytecode.Toplevel.execute.
      • Prove that compilation preserves semantics: if eval produces (val, prop),
        then execute on the compiled bytecode produces val.flatten (assuming prop
        holds).

      Phase 6: Byte and u32 operations

      • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
      • Specs for u32_less_than.
      • These require modeling the Goldilocks field arithmetic precisely.

      Relation to CSLib

      The approach is analogous to how CSLib provides
      foundational definitions for formalizing computer science in Lean. Where CSLib defines
      evaluation semantics for lambda calculus and other foundational languages, we define
      evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
      is: define a clean denotational/operational semantics first, then prove properties
      against it, then (optionally) prove that concrete implementations refine the semantics.

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          Skip to content

          Formally verifying Aiur programs #340

          Description

          @arthurpaulino

          Formal Evaluation Semantics for Aiur

          Goal

          Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
          Terms — so that we can state and prove theorems about the behavior of Aiur programs.
          This is not meant to be efficient; it is a specification against which the bytecode
          compiler and the Rust runtime can be verified.

          Overview

          The core idea: given a Toplevel (the set of all function/datatype declarations) and
          a Function, evaluating that function on concrete Value arguments produces a
          Result containing:

          • An output Value (the return value).
          • A Prop-valued side condition accumulating all assertEq obligations.

          This lets us state theorems like:

          -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
          theorem foo_spec (top : Toplevel) (x : G) :
          Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
          

          Core Definitions

          Values

          Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
          is fully reduced:

          inductiveValuewhere
          | unit : Value
          | field : G → Value
          | tuple : Array Value → Value
          | array : Array Value → Value
          deriving Repr, BEq, Inhabited

          Memory

          Store/load operations require a heap model. A Memory is a simple map from addresses
          to values:

          structureMemorywherecells : Array Value
          deriving Inhabited
          defMemory.store (mem : Memory) (v : Value) : G × Memory :=
          let addr := G.ofNat mem.cells.size
          (addr, { cells := mem.cells.push v })
          defMemory.load (mem : Memory) (addr : G) : Option Value :=
          mem.cells[addr.val.toNat]?

          Result and EvalState

          Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
          and stateful (memory, IO buffers). We track:

          structureEvalStatewherememory : Memory
          ioBuffer : IOBuffer
          fuel : Nat -- recursion boundinductiveResultwhere
          | ok : Value → Prop → Result -- value + accumulated assertions
          | fail : String → Result -- stuck / out of fuel / match failure

          The Prop component in ok is the conjunction of all assertEq conditions
          encountered during evaluation. When there are no assertions, this is True.

          Environment

          An Env maps local variables to values. Function lookup goes through the Toplevel:

          abbrev Env := Std.HashMap Local Value
          deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
          top.functions.find? fun f => f.name == name

          The Evaluator

          partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
          Term → EvalState × Result

          The key cases:

          TermSemantics
          .var xLook up x in env
          .data (.field g).ok (.field g) True
          .data (.tuple ts)Evaluate each element, combine
          .data (.array ts)Evaluate each element, combine
          .ret tEvaluate t (marks escaping to caller)
          .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
          .match t branchesEvaluate t, try each (pat, body) in order
          .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
          .add a bEvaluate both to fields, return field addition (mod gSize)
          .sub a bField subtraction
          .mul a bField multiplication
          .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
          .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
          .store tEvaluate t, push to memory, return pointer field
          .load tEvaluate t to a field (address), look up in memory
          .proj t iEvaluate t to a tuple, project component i
          .get t iEvaluate t to an array, index at i
          .slice t i jEvaluate t to an array, extract [i, j)
          .set t i vEvaluate t to an array, evaluate v, replace at i

          For function calls, fuel is decremented. If fuel reaches 0, the result is
          .fail "out of fuel". This makes eval total.

          Pattern Matching

          defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

          This attempts to destructure v according to pat, returning bindings on success.
          For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
          succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
          recursively matches sub-patterns.

          assertEq Accumulation

          The crucial design point: assertEq does not fail evaluation. Instead, the equality
          is accumulated as a Prop in the result:

          | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
          let (state, b_val, b_prop) ← evalValue top env state b
          let (state, ret_val, ret_prop) ← eval top env state ret
          let assertion := (a_val = b_val)
          (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

          This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
          The evaluator produces the output regardless, and the proposition captures what must
          hold for the execution to be valid.

          Flattening to Array G

          To connect eval to the existing test infrastructure (which works with Array G),
          define:

          defValue.flatten : Value → Array G
          | .unit => #[]
          | .field g => #[g]
          | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
          | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

          And a top-level entry point:

          defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
          (fuel : Nat := 1000) : Option (Array G × Prop) :=
          let func ← lookupFunction top funcName
          let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
          let state := { memory := default, ioBuffer := default, fuel }
          match eval top env state func.body with
          | (_, .ok val prop) => some (val.flatten, prop)
          | _ => none

          Stating Theorems

          With this setup, we can write specifications like:

          -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
          (h_func : lookupFunction top `foo = some foo_fn) :
          Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
          ...
          -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
          (h_func : lookupFunction top `sum = some sum_fn) :
          Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
          ...
          -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
          Aiur.run top `abs_or_zero #[x] =
          if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
          ...

          Key Design Decisions

          1. Prop-valued assertions, not runtime failure

          assertEq accumulates a Prop rather than causing eval to fail. This is essential
          because Aiur is a circuit language: the prover always produces an output, and the
          constraints determine whether the output is valid. Separating the output from the
          validity condition is what makes formal verification useful — you can reason about what
          the circuit computes independently of whether the constraints are satisfied.

          2. Fuel-based termination

          Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
          are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
          bounded recursion.

          3. Operating on Term (pre-simplification)

          eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
          means it can be used to specify the semantics of Aiur programs as written. A separate
          correctness theorem would relate eval on Term to execution of compiled Bytecode.

          4. Memory as an array

          The memory model is a simple append-only array. store appends and returns the index
          as a G. load indexes into the array. This is adequate because Aiur memory is
          append-only (no mutation of existing cells).

          Verification Roadmap

          Phase 1: Definitional interpreter (Aiur.eval)

          • Define Value, Memory, EvalState, Result, Env.
          • Implement eval, matchPattern, Value.flatten.
          • Implement Aiur.run as the top-level entry point.

          Phase 2: Basic specifications

          • State and prove specs for simple functions: id, sum, prod.
          • State and prove specs involving assertEq.
          • State and prove specs involving match on field values.

          Phase 3: Data structure operations

          • Specs for proj, get, slice, set.
          • Specs for tuple/array construction and destructuring.
          • Specs involving store/load.

          Phase 4: Recursion and datatypes

          • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
          • Specs for enum construction and pattern matching (Shape, Nat).
          • Mutual recursion (even/odd).

          Phase 5: Compiler correctness

          • Define a relation between eval on Term and Bytecode.Toplevel.execute.
          • Prove that compilation preserves semantics: if eval produces (val, prop),
            then execute on the compiled bytecode produces val.flatten (assuming prop
            holds).

          Phase 6: Byte and u32 operations

          • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
          • Specs for u32_less_than.
          • These require modeling the Goldilocks field arithmetic precisely.

          Relation to CSLib

          The approach is analogous to how CSLib provides
          foundational definitions for formalizing computer science in Lean. Where CSLib defines
          evaluation semantics for lambda calculus and other foundational languages, we define
          evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
          is: define a clean denotational/operational semantics first, then prove properties
          against it, then (optionally) prove that concrete implementations refine the semantics.

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Formally verifying Aiur programs #340

              Description

              @arthurpaulino

              Formal Evaluation Semantics for Aiur

              Goal

              Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
              Terms — so that we can state and prove theorems about the behavior of Aiur programs.
              This is not meant to be efficient; it is a specification against which the bytecode
              compiler and the Rust runtime can be verified.

              Overview

              The core idea: given a Toplevel (the set of all function/datatype declarations) and
              a Function, evaluating that function on concrete Value arguments produces a
              Result containing:

              • An output Value (the return value).
              • A Prop-valued side condition accumulating all assertEq obligations.

              This lets us state theorems like:

              -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
              theorem foo_spec (top : Toplevel) (x : G) :
              Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
              

              Core Definitions

              Values

              Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
              is fully reduced:

              inductiveValuewhere
              | unit : Value
              | field : G → Value
              | tuple : Array Value → Value
              | array : Array Value → Value
              deriving Repr, BEq, Inhabited

              Memory

              Store/load operations require a heap model. A Memory is a simple map from addresses
              to values:

              structureMemorywherecells : Array Value
              deriving Inhabited
              defMemory.store (mem : Memory) (v : Value) : G × Memory :=
              let addr := G.ofNat mem.cells.size
              (addr, { cells := mem.cells.push v })
              defMemory.load (mem : Memory) (addr : G) : Option Value :=
              mem.cells[addr.val.toNat]?

              Result and EvalState

              Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
              and stateful (memory, IO buffers). We track:

              structureEvalStatewherememory : Memory
              ioBuffer : IOBuffer
              fuel : Nat -- recursion boundinductiveResultwhere
              | ok : Value → Prop → Result -- value + accumulated assertions
              | fail : String → Result -- stuck / out of fuel / match failure

              The Prop component in ok is the conjunction of all assertEq conditions
              encountered during evaluation. When there are no assertions, this is True.

              Environment

              An Env maps local variables to values. Function lookup goes through the Toplevel:

              abbrev Env := Std.HashMap Local Value
              deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
              top.functions.find? fun f => f.name == name

              The Evaluator

              partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
              Term → EvalState × Result

              The key cases:

              TermSemantics
              .var xLook up x in env
              .data (.field g).ok (.field g) True
              .data (.tuple ts)Evaluate each element, combine
              .data (.array ts)Evaluate each element, combine
              .ret tEvaluate t (marks escaping to caller)
              .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
              .match t branchesEvaluate t, try each (pat, body) in order
              .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
              .add a bEvaluate both to fields, return field addition (mod gSize)
              .sub a bField subtraction
              .mul a bField multiplication
              .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
              .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
              .store tEvaluate t, push to memory, return pointer field
              .load tEvaluate t to a field (address), look up in memory
              .proj t iEvaluate t to a tuple, project component i
              .get t iEvaluate t to an array, index at i
              .slice t i jEvaluate t to an array, extract [i, j)
              .set t i vEvaluate t to an array, evaluate v, replace at i

              For function calls, fuel is decremented. If fuel reaches 0, the result is
              .fail "out of fuel". This makes eval total.

              Pattern Matching

              defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

              This attempts to destructure v according to pat, returning bindings on success.
              For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
              succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
              recursively matches sub-patterns.

              assertEq Accumulation

              The crucial design point: assertEq does not fail evaluation. Instead, the equality
              is accumulated as a Prop in the result:

              | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
              let (state, b_val, b_prop) ← evalValue top env state b
              let (state, ret_val, ret_prop) ← eval top env state ret
              let assertion := (a_val = b_val)
              (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

              This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
              The evaluator produces the output regardless, and the proposition captures what must
              hold for the execution to be valid.

              Flattening to Array G

              To connect eval to the existing test infrastructure (which works with Array G),
              define:

              defValue.flatten : Value → Array G
              | .unit => #[]
              | .field g => #[g]
              | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
              | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

              And a top-level entry point:

              defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
              (fuel : Nat := 1000) : Option (Array G × Prop) :=
              let func ← lookupFunction top funcName
              let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
              let state := { memory := default, ioBuffer := default, fuel }
              match eval top env state func.body with
              | (_, .ok val prop) => some (val.flatten, prop)
              | _ => none

              Stating Theorems

              With this setup, we can write specifications like:

              -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
              (h_func : lookupFunction top `foo = some foo_fn) :
              Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
              ...
              -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
              (h_func : lookupFunction top `sum = some sum_fn) :
              Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
              ...
              -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
              Aiur.run top `abs_or_zero #[x] =
              if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
              ...

              Key Design Decisions

              1. Prop-valued assertions, not runtime failure

              assertEq accumulates a Prop rather than causing eval to fail. This is essential
              because Aiur is a circuit language: the prover always produces an output, and the
              constraints determine whether the output is valid. Separating the output from the
              validity condition is what makes formal verification useful — you can reason about what
              the circuit computes independently of whether the constraints are satisfied.

              2. Fuel-based termination

              Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
              are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
              bounded recursion.

              3. Operating on Term (pre-simplification)

              eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
              means it can be used to specify the semantics of Aiur programs as written. A separate
              correctness theorem would relate eval on Term to execution of compiled Bytecode.

              4. Memory as an array

              The memory model is a simple append-only array. store appends and returns the index
              as a G. load indexes into the array. This is adequate because Aiur memory is
              append-only (no mutation of existing cells).

              Verification Roadmap

              Phase 1: Definitional interpreter (Aiur.eval)

              • Define Value, Memory, EvalState, Result, Env.
              • Implement eval, matchPattern, Value.flatten.
              • Implement Aiur.run as the top-level entry point.

              Phase 2: Basic specifications

              • State and prove specs for simple functions: id, sum, prod.
              • State and prove specs involving assertEq.
              • State and prove specs involving match on field values.

              Phase 3: Data structure operations

              • Specs for proj, get, slice, set.
              • Specs for tuple/array construction and destructuring.
              • Specs involving store/load.

              Phase 4: Recursion and datatypes

              • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
              • Specs for enum construction and pattern matching (Shape, Nat).
              • Mutual recursion (even/odd).

              Phase 5: Compiler correctness

              • Define a relation between eval on Term and Bytecode.Toplevel.execute.
              • Prove that compilation preserves semantics: if eval produces (val, prop),
                then execute on the compiled bytecode produces val.flatten (assuming prop
                holds).

              Phase 6: Byte and u32 operations

              • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
              • Specs for u32_less_than.
              • These require modeling the Goldilocks field arithmetic precisely.

              Relation to CSLib

              The approach is analogous to how CSLib provides
              foundational definitions for formalizing computer science in Lean. Where CSLib defines
              evaluation semantics for lambda calculus and other foundational languages, we define
              evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
              is: define a clean denotational/operational semantics first, then prove properties
              against it, then (optionally) prove that concrete implementations refine the semantics.

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  Skip to content

                  Formally verifying Aiur programs #340

                  Description

                  @arthurpaulino

                  Formal Evaluation Semantics for Aiur

                  Goal

                  Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
                  Terms — so that we can state and prove theorems about the behavior of Aiur programs.
                  This is not meant to be efficient; it is a specification against which the bytecode
                  compiler and the Rust runtime can be verified.

                  Overview

                  The core idea: given a Toplevel (the set of all function/datatype declarations) and
                  a Function, evaluating that function on concrete Value arguments produces a
                  Result containing:

                  • An output Value (the return value).
                  • A Prop-valued side condition accumulating all assertEq obligations.

                  This lets us state theorems like:

                  -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
                  theorem foo_spec (top : Toplevel) (x : G) :
                  Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
                  

                  Core Definitions

                  Values

                  Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
                  is fully reduced:

                  inductiveValuewhere
                  | unit : Value
                  | field : G → Value
                  | tuple : Array Value → Value
                  | array : Array Value → Value
                  deriving Repr, BEq, Inhabited

                  Memory

                  Store/load operations require a heap model. A Memory is a simple map from addresses
                  to values:

                  structureMemorywherecells : Array Value
                  deriving Inhabited
                  defMemory.store (mem : Memory) (v : Value) : G × Memory :=
                  let addr := G.ofNat mem.cells.size
                  (addr, { cells := mem.cells.push v })
                  defMemory.load (mem : Memory) (addr : G) : Option Value :=
                  mem.cells[addr.val.toNat]?

                  Result and EvalState

                  Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
                  and stateful (memory, IO buffers). We track:

                  structureEvalStatewherememory : Memory
                  ioBuffer : IOBuffer
                  fuel : Nat -- recursion boundinductiveResultwhere
                  | ok : Value → Prop → Result -- value + accumulated assertions
                  | fail : String → Result -- stuck / out of fuel / match failure

                  The Prop component in ok is the conjunction of all assertEq conditions
                  encountered during evaluation. When there are no assertions, this is True.

                  Environment

                  An Env maps local variables to values. Function lookup goes through the Toplevel:

                  abbrev Env := Std.HashMap Local Value
                  deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
                  top.functions.find? fun f => f.name == name

                  The Evaluator

                  partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
                  Term → EvalState × Result

                  The key cases:

                  TermSemantics
                  .var xLook up x in env
                  .data (.field g).ok (.field g) True
                  .data (.tuple ts)Evaluate each element, combine
                  .data (.array ts)Evaluate each element, combine
                  .ret tEvaluate t (marks escaping to caller)
                  .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
                  .match t branchesEvaluate t, try each (pat, body) in order
                  .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
                  .add a bEvaluate both to fields, return field addition (mod gSize)
                  .sub a bField subtraction
                  .mul a bField multiplication
                  .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
                  .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
                  .store tEvaluate t, push to memory, return pointer field
                  .load tEvaluate t to a field (address), look up in memory
                  .proj t iEvaluate t to a tuple, project component i
                  .get t iEvaluate t to an array, index at i
                  .slice t i jEvaluate t to an array, extract [i, j)
                  .set t i vEvaluate t to an array, evaluate v, replace at i

                  For function calls, fuel is decremented. If fuel reaches 0, the result is
                  .fail "out of fuel". This makes eval total.

                  Pattern Matching

                  defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

                  This attempts to destructure v according to pat, returning bindings on success.
                  For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
                  succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
                  recursively matches sub-patterns.

                  assertEq Accumulation

                  The crucial design point: assertEq does not fail evaluation. Instead, the equality
                  is accumulated as a Prop in the result:

                  | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
                  let (state, b_val, b_prop) ← evalValue top env state b
                  let (state, ret_val, ret_prop) ← eval top env state ret
                  let assertion := (a_val = b_val)
                  (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

                  This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
                  The evaluator produces the output regardless, and the proposition captures what must
                  hold for the execution to be valid.

                  Flattening to Array G

                  To connect eval to the existing test infrastructure (which works with Array G),
                  define:

                  defValue.flatten : Value → Array G
                  | .unit => #[]
                  | .field g => #[g]
                  | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
                  | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

                  And a top-level entry point:

                  defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
                  (fuel : Nat := 1000) : Option (Array G × Prop) :=
                  let func ← lookupFunction top funcName
                  let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
                  let state := { memory := default, ioBuffer := default, fuel }
                  match eval top env state func.body with
                  | (_, .ok val prop) => some (val.flatten, prop)
                  | _ => none

                  Stating Theorems

                  With this setup, we can write specifications like:

                  -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
                  (h_func : lookupFunction top `foo = some foo_fn) :
                  Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
                  ...
                  -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
                  (h_func : lookupFunction top `sum = some sum_fn) :
                  Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
                  ...
                  -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
                  Aiur.run top `abs_or_zero #[x] =
                  if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
                  ...

                  Key Design Decisions

                  1. Prop-valued assertions, not runtime failure

                  assertEq accumulates a Prop rather than causing eval to fail. This is essential
                  because Aiur is a circuit language: the prover always produces an output, and the
                  constraints determine whether the output is valid. Separating the output from the
                  validity condition is what makes formal verification useful — you can reason about what
                  the circuit computes independently of whether the constraints are satisfied.

                  2. Fuel-based termination

                  Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
                  are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
                  bounded recursion.

                  3. Operating on Term (pre-simplification)

                  eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
                  means it can be used to specify the semantics of Aiur programs as written. A separate
                  correctness theorem would relate eval on Term to execution of compiled Bytecode.

                  4. Memory as an array

                  The memory model is a simple append-only array. store appends and returns the index
                  as a G. load indexes into the array. This is adequate because Aiur memory is
                  append-only (no mutation of existing cells).

                  Verification Roadmap

                  Phase 1: Definitional interpreter (Aiur.eval)

                  • Define Value, Memory, EvalState, Result, Env.
                  • Implement eval, matchPattern, Value.flatten.
                  • Implement Aiur.run as the top-level entry point.

                  Phase 2: Basic specifications

                  • State and prove specs for simple functions: id, sum, prod.
                  • State and prove specs involving assertEq.
                  • State and prove specs involving match on field values.

                  Phase 3: Data structure operations

                  • Specs for proj, get, slice, set.
                  • Specs for tuple/array construction and destructuring.
                  • Specs involving store/load.

                  Phase 4: Recursion and datatypes

                  • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
                  • Specs for enum construction and pattern matching (Shape, Nat).
                  • Mutual recursion (even/odd).

                  Phase 5: Compiler correctness

                  • Define a relation between eval on Term and Bytecode.Toplevel.execute.
                  • Prove that compilation preserves semantics: if eval produces (val, prop),
                    then execute on the compiled bytecode produces val.flatten (assuming prop
                    holds).

                  Phase 6: Byte and u32 operations

                  • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
                  • Specs for u32_less_than.
                  • These require modeling the Goldilocks field arithmetic precisely.

                  Relation to CSLib

                  The approach is analogous to how CSLib provides
                  foundational definitions for formalizing computer science in Lean. Where CSLib defines
                  evaluation semantics for lambda calculus and other foundational languages, we define
                  evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
                  is: define a clean denotational/operational semantics first, then prove properties
                  against it, then (optionally) prove that concrete implementations refine the semantics.

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      Skip to content

                      Formally verifying Aiur programs #340

                      Description

                      @arthurpaulino

                      Formal Evaluation Semantics for Aiur

                      Goal

                      Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
                      Terms — so that we can state and prove theorems about the behavior of Aiur programs.
                      This is not meant to be efficient; it is a specification against which the bytecode
                      compiler and the Rust runtime can be verified.

                      Overview

                      The core idea: given a Toplevel (the set of all function/datatype declarations) and
                      a Function, evaluating that function on concrete Value arguments produces a
                      Result containing:

                      • An output Value (the return value).
                      • A Prop-valued side condition accumulating all assertEq obligations.

                      This lets us state theorems like:

                      -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
                      theorem foo_spec (top : Toplevel) (x : G) :
                      Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
                      

                      Core Definitions

                      Values

                      Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
                      is fully reduced:

                      inductiveValuewhere
                      | unit : Value
                      | field : G → Value
                      | tuple : Array Value → Value
                      | array : Array Value → Value
                      deriving Repr, BEq, Inhabited

                      Memory

                      Store/load operations require a heap model. A Memory is a simple map from addresses
                      to values:

                      structureMemorywherecells : Array Value
                      deriving Inhabited
                      defMemory.store (mem : Memory) (v : Value) : G × Memory :=
                      let addr := G.ofNat mem.cells.size
                      (addr, { cells := mem.cells.push v })
                      defMemory.load (mem : Memory) (addr : G) : Option Value :=
                      mem.cells[addr.val.toNat]?

                      Result and EvalState

                      Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
                      and stateful (memory, IO buffers). We track:

                      structureEvalStatewherememory : Memory
                      ioBuffer : IOBuffer
                      fuel : Nat -- recursion boundinductiveResultwhere
                      | ok : Value → Prop → Result -- value + accumulated assertions
                      | fail : String → Result -- stuck / out of fuel / match failure

                      The Prop component in ok is the conjunction of all assertEq conditions
                      encountered during evaluation. When there are no assertions, this is True.

                      Environment

                      An Env maps local variables to values. Function lookup goes through the Toplevel:

                      abbrev Env := Std.HashMap Local Value
                      deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
                      top.functions.find? fun f => f.name == name

                      The Evaluator

                      partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
                      Term → EvalState × Result

                      The key cases:

                      TermSemantics
                      .var xLook up x in env
                      .data (.field g).ok (.field g) True
                      .data (.tuple ts)Evaluate each element, combine
                      .data (.array ts)Evaluate each element, combine
                      .ret tEvaluate t (marks escaping to caller)
                      .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
                      .match t branchesEvaluate t, try each (pat, body) in order
                      .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
                      .add a bEvaluate both to fields, return field addition (mod gSize)
                      .sub a bField subtraction
                      .mul a bField multiplication
                      .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
                      .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
                      .store tEvaluate t, push to memory, return pointer field
                      .load tEvaluate t to a field (address), look up in memory
                      .proj t iEvaluate t to a tuple, project component i
                      .get t iEvaluate t to an array, index at i
                      .slice t i jEvaluate t to an array, extract [i, j)
                      .set t i vEvaluate t to an array, evaluate v, replace at i

                      For function calls, fuel is decremented. If fuel reaches 0, the result is
                      .fail "out of fuel". This makes eval total.

                      Pattern Matching

                      defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

                      This attempts to destructure v according to pat, returning bindings on success.
                      For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
                      succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
                      recursively matches sub-patterns.

                      assertEq Accumulation

                      The crucial design point: assertEq does not fail evaluation. Instead, the equality
                      is accumulated as a Prop in the result:

                      | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
                      let (state, b_val, b_prop) ← evalValue top env state b
                      let (state, ret_val, ret_prop) ← eval top env state ret
                      let assertion := (a_val = b_val)
                      (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

                      This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
                      The evaluator produces the output regardless, and the proposition captures what must
                      hold for the execution to be valid.

                      Flattening to Array G

                      To connect eval to the existing test infrastructure (which works with Array G),
                      define:

                      defValue.flatten : Value → Array G
                      | .unit => #[]
                      | .field g => #[g]
                      | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
                      | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

                      And a top-level entry point:

                      defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
                      (fuel : Nat := 1000) : Option (Array G × Prop) :=
                      let func ← lookupFunction top funcName
                      let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
                      let state := { memory := default, ioBuffer := default, fuel }
                      match eval top env state func.body with
                      | (_, .ok val prop) => some (val.flatten, prop)
                      | _ => none

                      Stating Theorems

                      With this setup, we can write specifications like:

                      -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
                      (h_func : lookupFunction top `foo = some foo_fn) :
                      Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
                      ...
                      -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
                      (h_func : lookupFunction top `sum = some sum_fn) :
                      Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
                      ...
                      -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
                      Aiur.run top `abs_or_zero #[x] =
                      if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
                      ...

                      Key Design Decisions

                      1. Prop-valued assertions, not runtime failure

                      assertEq accumulates a Prop rather than causing eval to fail. This is essential
                      because Aiur is a circuit language: the prover always produces an output, and the
                      constraints determine whether the output is valid. Separating the output from the
                      validity condition is what makes formal verification useful — you can reason about what
                      the circuit computes independently of whether the constraints are satisfied.

                      2. Fuel-based termination

                      Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
                      are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
                      bounded recursion.

                      3. Operating on Term (pre-simplification)

                      eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
                      means it can be used to specify the semantics of Aiur programs as written. A separate
                      correctness theorem would relate eval on Term to execution of compiled Bytecode.

                      4. Memory as an array

                      The memory model is a simple append-only array. store appends and returns the index
                      as a G. load indexes into the array. This is adequate because Aiur memory is
                      append-only (no mutation of existing cells).

                      Verification Roadmap

                      Phase 1: Definitional interpreter (Aiur.eval)

                      • Define Value, Memory, EvalState, Result, Env.
                      • Implement eval, matchPattern, Value.flatten.
                      • Implement Aiur.run as the top-level entry point.

                      Phase 2: Basic specifications

                      • State and prove specs for simple functions: id, sum, prod.
                      • State and prove specs involving assertEq.
                      • State and prove specs involving match on field values.

                      Phase 3: Data structure operations

                      • Specs for proj, get, slice, set.
                      • Specs for tuple/array construction and destructuring.
                      • Specs involving store/load.

                      Phase 4: Recursion and datatypes

                      • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
                      • Specs for enum construction and pattern matching (Shape, Nat).
                      • Mutual recursion (even/odd).

                      Phase 5: Compiler correctness

                      • Define a relation between eval on Term and Bytecode.Toplevel.execute.
                      • Prove that compilation preserves semantics: if eval produces (val, prop),
                        then execute on the compiled bytecode produces val.flatten (assuming prop
                        holds).

                      Phase 6: Byte and u32 operations

                      • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
                      • Specs for u32_less_than.
                      • These require modeling the Goldilocks field arithmetic precisely.

                      Relation to CSLib

                      The approach is analogous to how CSLib provides
                      foundational definitions for formalizing computer science in Lean. Where CSLib defines
                      evaluation semantics for lambda calculus and other foundational languages, we define
                      evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
                      is: define a clean denotational/operational semantics first, then prove properties
                      against it, then (optionally) prove that concrete implementations refine the semantics.

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          Skip to content

                          Formally verifying Aiur programs #340

                          Description

                          @arthurpaulino

                          Formal Evaluation Semantics for Aiur

                          Goal

                          Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
                          Terms — so that we can state and prove theorems about the behavior of Aiur programs.
                          This is not meant to be efficient; it is a specification against which the bytecode
                          compiler and the Rust runtime can be verified.

                          Overview

                          The core idea: given a Toplevel (the set of all function/datatype declarations) and
                          a Function, evaluating that function on concrete Value arguments produces a
                          Result containing:

                          • An output Value (the return value).
                          • A Prop-valued side condition accumulating all assertEq obligations.

                          This lets us state theorems like:

                          -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
                          theorem foo_spec (top : Toplevel) (x : G) :
                          Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
                          

                          Core Definitions

                          Values

                          Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
                          is fully reduced:

                          inductiveValuewhere
                          | unit : Value
                          | field : G → Value
                          | tuple : Array Value → Value
                          | array : Array Value → Value
                          deriving Repr, BEq, Inhabited

                          Memory

                          Store/load operations require a heap model. A Memory is a simple map from addresses
                          to values:

                          structureMemorywherecells : Array Value
                          deriving Inhabited
                          defMemory.store (mem : Memory) (v : Value) : G × Memory :=
                          let addr := G.ofNat mem.cells.size
                          (addr, { cells := mem.cells.push v })
                          defMemory.load (mem : Memory) (addr : G) : Option Value :=
                          mem.cells[addr.val.toNat]?

                          Result and EvalState

                          Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
                          and stateful (memory, IO buffers). We track:

                          structureEvalStatewherememory : Memory
                          ioBuffer : IOBuffer
                          fuel : Nat -- recursion boundinductiveResultwhere
                          | ok : Value → Prop → Result -- value + accumulated assertions
                          | fail : String → Result -- stuck / out of fuel / match failure

                          The Prop component in ok is the conjunction of all assertEq conditions
                          encountered during evaluation. When there are no assertions, this is True.

                          Environment

                          An Env maps local variables to values. Function lookup goes through the Toplevel:

                          abbrev Env := Std.HashMap Local Value
                          deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
                          top.functions.find? fun f => f.name == name

                          The Evaluator

                          partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
                          Term → EvalState × Result

                          The key cases:

                          TermSemantics
                          .var xLook up x in env
                          .data (.field g).ok (.field g) True
                          .data (.tuple ts)Evaluate each element, combine
                          .data (.array ts)Evaluate each element, combine
                          .ret tEvaluate t (marks escaping to caller)
                          .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
                          .match t branchesEvaluate t, try each (pat, body) in order
                          .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
                          .add a bEvaluate both to fields, return field addition (mod gSize)
                          .sub a bField subtraction
                          .mul a bField multiplication
                          .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
                          .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
                          .store tEvaluate t, push to memory, return pointer field
                          .load tEvaluate t to a field (address), look up in memory
                          .proj t iEvaluate t to a tuple, project component i
                          .get t iEvaluate t to an array, index at i
                          .slice t i jEvaluate t to an array, extract [i, j)
                          .set t i vEvaluate t to an array, evaluate v, replace at i

                          For function calls, fuel is decremented. If fuel reaches 0, the result is
                          .fail "out of fuel". This makes eval total.

                          Pattern Matching

                          defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

                          This attempts to destructure v according to pat, returning bindings on success.
                          For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
                          succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
                          recursively matches sub-patterns.

                          assertEq Accumulation

                          The crucial design point: assertEq does not fail evaluation. Instead, the equality
                          is accumulated as a Prop in the result:

                          | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
                          let (state, b_val, b_prop) ← evalValue top env state b
                          let (state, ret_val, ret_prop) ← eval top env state ret
                          let assertion := (a_val = b_val)
                          (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

                          This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
                          The evaluator produces the output regardless, and the proposition captures what must
                          hold for the execution to be valid.

                          Flattening to Array G

                          To connect eval to the existing test infrastructure (which works with Array G),
                          define:

                          defValue.flatten : Value → Array G
                          | .unit => #[]
                          | .field g => #[g]
                          | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
                          | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

                          And a top-level entry point:

                          defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
                          (fuel : Nat := 1000) : Option (Array G × Prop) :=
                          let func ← lookupFunction top funcName
                          let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
                          let state := { memory := default, ioBuffer := default, fuel }
                          match eval top env state func.body with
                          | (_, .ok val prop) => some (val.flatten, prop)
                          | _ => none

                          Stating Theorems

                          With this setup, we can write specifications like:

                          -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
                          (h_func : lookupFunction top `foo = some foo_fn) :
                          Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
                          ...
                          -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
                          (h_func : lookupFunction top `sum = some sum_fn) :
                          Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
                          ...
                          -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
                          Aiur.run top `abs_or_zero #[x] =
                          if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
                          ...

                          Key Design Decisions

                          1. Prop-valued assertions, not runtime failure

                          assertEq accumulates a Prop rather than causing eval to fail. This is essential
                          because Aiur is a circuit language: the prover always produces an output, and the
                          constraints determine whether the output is valid. Separating the output from the
                          validity condition is what makes formal verification useful — you can reason about what
                          the circuit computes independently of whether the constraints are satisfied.

                          2. Fuel-based termination

                          Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
                          are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
                          bounded recursion.

                          3. Operating on Term (pre-simplification)

                          eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
                          means it can be used to specify the semantics of Aiur programs as written. A separate
                          correctness theorem would relate eval on Term to execution of compiled Bytecode.

                          4. Memory as an array

                          The memory model is a simple append-only array. store appends and returns the index
                          as a G. load indexes into the array. This is adequate because Aiur memory is
                          append-only (no mutation of existing cells).

                          Verification Roadmap

                          Phase 1: Definitional interpreter (Aiur.eval)

                          • Define Value, Memory, EvalState, Result, Env.
                          • Implement eval, matchPattern, Value.flatten.
                          • Implement Aiur.run as the top-level entry point.

                          Phase 2: Basic specifications

                          • State and prove specs for simple functions: id, sum, prod.
                          • State and prove specs involving assertEq.
                          • State and prove specs involving match on field values.

                          Phase 3: Data structure operations

                          • Specs for proj, get, slice, set.
                          • Specs for tuple/array construction and destructuring.
                          • Specs involving store/load.

                          Phase 4: Recursion and datatypes

                          • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
                          • Specs for enum construction and pattern matching (Shape, Nat).
                          • Mutual recursion (even/odd).

                          Phase 5: Compiler correctness

                          • Define a relation between eval on Term and Bytecode.Toplevel.execute.
                          • Prove that compilation preserves semantics: if eval produces (val, prop),
                            then execute on the compiled bytecode produces val.flatten (assuming prop
                            holds).

                          Phase 6: Byte and u32 operations

                          • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
                          • Specs for u32_less_than.
                          • These require modeling the Goldilocks field arithmetic precisely.

                          Relation to CSLib

                          The approach is analogous to how CSLib provides
                          foundational definitions for formalizing computer science in Lean. Where CSLib defines
                          evaluation semantics for lambda calculus and other foundational languages, we define
                          evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
                          is: define a clean denotational/operational semantics first, then prove properties
                          against it, then (optionally) prove that concrete implementations refine the semantics.

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              Skip to content

                              Formally verifying Aiur programs #340

                              Description

                              @arthurpaulino

                              Formal Evaluation Semantics for Aiur

                              Goal

                              Define an Aiur.eval function in Lean — a pure, definitional interpreter for Aiur
                              Terms — so that we can state and prove theorems about the behavior of Aiur programs.
                              This is not meant to be efficient; it is a specification against which the bytecode
                              compiler and the Rust runtime can be verified.

                              Overview

                              The core idea: given a Toplevel (the set of all function/datatype declarations) and
                              a Function, evaluating that function on concrete Value arguments produces a
                              Result containing:

                              • An output Value (the return value).
                              • A Prop-valued side condition accumulating all assertEq obligations.

                              This lets us state theorems like:

                              -- If `foo_fn` does `assert_eq!(x, 0); 2 * x`:
                              theorem foo_spec (top : Toplevel) (x : G) :
                              Aiur.eval top `foo_fn [.field x] = .ok (.field (2 * x)) (x = 0)
                              

                              Core Definitions

                              Values

                              Value is the semantic domain — what Aiur terms evaluate to. It mirrors Data but
                              is fully reduced:

                              inductiveValuewhere
                              | unit : Value
                              | field : G → Value
                              | tuple : Array Value → Value
                              | array : Array Value → Value
                              deriving Repr, BEq, Inhabited

                              Memory

                              Store/load operations require a heap model. A Memory is a simple map from addresses
                              to values:

                              structureMemorywherecells : Array Value
                              deriving Inhabited
                              defMemory.store (mem : Memory) (v : Value) : G × Memory :=
                              let addr := G.ofNat mem.cells.size
                              (addr, { cells := mem.cells.push v })
                              defMemory.load (mem : Memory) (addr : G) : Option Value :=
                              mem.cells[addr.val.toNat]?

                              Result and EvalState

                              Evaluation is partial (recursion may diverge, pattern matches may be non-exhaustive)
                              and stateful (memory, IO buffers). We track:

                              structureEvalStatewherememory : Memory
                              ioBuffer : IOBuffer
                              fuel : Nat -- recursion boundinductiveResultwhere
                              | ok : Value → Prop → Result -- value + accumulated assertions
                              | fail : String → Result -- stuck / out of fuel / match failure

                              The Prop component in ok is the conjunction of all assertEq conditions
                              encountered during evaluation. When there are no assertions, this is True.

                              Environment

                              An Env maps local variables to values. Function lookup goes through the Toplevel:

                              abbrev Env := Std.HashMap Local Value
                              deflookupFunction (top : Toplevel) (name : Global) : Option Function :=
                              top.functions.find? fun f => f.name == name

                              The Evaluator

                              partial defeval (top : Toplevel) (env : Env) (state : EvalState) :
                              Term → EvalState × Result

                              The key cases:

                              TermSemantics
                              .var xLook up x in env
                              .data (.field g).ok (.field g) True
                              .data (.tuple ts)Evaluate each element, combine
                              .data (.array ts)Evaluate each element, combine
                              .ret tEvaluate t (marks escaping to caller)
                              .let pat val bodyEvaluate val, match against pat to extend env, evaluate body
                              .match t branchesEvaluate t, try each (pat, body) in order
                              .app name argsEvaluate args, look up function, substitute into body, recurse (decrement fuel)
                              .add a bEvaluate both to fields, return field addition (mod gSize)
                              .sub a bField subtraction
                              .mul a bField multiplication
                              .eqZero aEvaluate a to .field g, return .field (if g == 0 then 1 else 0)
                              .assertEq a b retEvaluate a, b, evaluate ret, return ret's value with (a_val = b_val) ∧ ret_prop
                              .store tEvaluate t, push to memory, return pointer field
                              .load tEvaluate t to a field (address), look up in memory
                              .proj t iEvaluate t to a tuple, project component i
                              .get t iEvaluate t to an array, index at i
                              .slice t i jEvaluate t to an array, extract [i, j)
                              .set t i vEvaluate t to an array, evaluate v, replace at i

                              For function calls, fuel is decremented. If fuel reaches 0, the result is
                              .fail "out of fuel". This makes eval total.

                              Pattern Matching

                              defmatchPattern (pat : Pattern) (v : Value) : Option (List (Local × Value))

                              This attempts to destructure v according to pat, returning bindings on success.
                              For Pattern.field g, it checks that v = .field g. For Pattern.var x, it always
                              succeeds with [(x, v)]. For constructors (Pattern.ref), it checks the tag and
                              recursively matches sub-patterns.

                              assertEq Accumulation

                              The crucial design point: assertEq does not fail evaluation. Instead, the equality
                              is accumulated as a Prop in the result:

                              | .assertEq a b ret => dolet (state, a_val, a_prop) ← evalValue top env state a
                              let (state, b_val, b_prop) ← evalValue top env state b
                              let (state, ret_val, ret_prop) ← eval top env state ret
                              let assertion := (a_val = b_val)
                              (state, .ok ret_val (a_prop ∧ b_prop ∧ assertion ∧ ret_prop))

                              This mirrors how the circuit works: assertEq is a constraint, not a runtime check.
                              The evaluator produces the output regardless, and the proposition captures what must
                              hold for the execution to be valid.

                              Flattening to Array G

                              To connect eval to the existing test infrastructure (which works with Array G),
                              define:

                              defValue.flatten : Value → Array G
                              | .unit => #[]
                              | .field g => #[g]
                              | .tuple vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten
                              | .array vs => vs.foldl (init := #[]) fun acc v => acc ++ v.flatten

                              And a top-level entry point:

                              defAiur.run (top : Toplevel) (funcName : Global) (args : Array G)
                              (fuel : Nat := 1000) : Option (Array G × Prop) :=
                              let func ← lookupFunction top funcName
                              let env := Env.ofList (func.inputs.zip (unflattenArgs func.inputs args))
                              let state := { memory := default, ioBuffer := default, fuel }
                              match eval top env state func.body with
                              | (_, .ok val prop) => some (val.flatten, prop)
                              | _ => none

                              Stating Theorems

                              With this setup, we can write specifications like:

                              -- A function that asserts its input is zero and doubles it-- fn foo(x: G) -> G { assert_eq!(x, 0); x + x }theoremfoo_correct (top : Toplevel) (x : G)
                              (h_func : lookupFunction top `foo = some foo_fn) :
                              Aiur.run top `foo #[x] = some (#[x + x], x = (0 : G)) := by
                              ...
                              -- A pure function with no assertions-- fn sum(x: G, y: G) -> G { x + y }theoremsum_correct (top : Toplevel) (x y : G)
                              (h_func : lookupFunction top `sum = some sum_fn) :
                              Aiur.run top `sum #[x, y] = some (#[x + y], True) := by
                              ...
                              -- Conditional: fn abs_or_zero(x: G) -> G { match eq_zero(x) { 1 => 0, _ => x } }theoremabs_or_zero_spec (top : Toplevel) (x : G) ... :
                              Aiur.run top `abs_or_zero #[x] =
                              if x = 0then some (#[(0 : G)], True) else some (#[x], True) := by
                              ...

                              Key Design Decisions

                              1. Prop-valued assertions, not runtime failure

                              assertEq accumulates a Prop rather than causing eval to fail. This is essential
                              because Aiur is a circuit language: the prover always produces an output, and the
                              constraints determine whether the output is valid. Separating the output from the
                              validity condition is what makes formal verification useful — you can reason about what
                              the circuit computes independently of whether the constraints are satisfied.

                              2. Fuel-based termination

                              Using a fuel : Nat parameter makes eval structurally recursive (total). Theorems
                              are stated with "sufficient fuel" hypotheses, or we prove that certain inputs require
                              bounded recursion.

                              3. Operating on Term (pre-simplification)

                              eval works on Term, the surface-level IR, not on TypedTerm or Bytecode. This
                              means it can be used to specify the semantics of Aiur programs as written. A separate
                              correctness theorem would relate eval on Term to execution of compiled Bytecode.

                              4. Memory as an array

                              The memory model is a simple append-only array. store appends and returns the index
                              as a G. load indexes into the array. This is adequate because Aiur memory is
                              append-only (no mutation of existing cells).

                              Verification Roadmap

                              Phase 1: Definitional interpreter (Aiur.eval)

                              • Define Value, Memory, EvalState, Result, Env.
                              • Implement eval, matchPattern, Value.flatten.
                              • Implement Aiur.run as the top-level entry point.

                              Phase 2: Basic specifications

                              • State and prove specs for simple functions: id, sum, prod.
                              • State and prove specs involving assertEq.
                              • State and prove specs involving match on field values.

                              Phase 3: Data structure operations

                              • Specs for proj, get, slice, set.
                              • Specs for tuple/array construction and destructuring.
                              • Specs involving store/load.

                              Phase 4: Recursion and datatypes

                              • Specs for recursive functions (factorial, fibonacci) with fuel analysis.
                              • Specs for enum construction and pattern matching (Shape, Nat).
                              • Mutual recursion (even/odd).

                              Phase 5: Compiler correctness

                              • Define a relation between eval on Term and Bytecode.Toplevel.execute.
                              • Prove that compilation preserves semantics: if eval produces (val, prop),
                                then execute on the compiled bytecode produces val.flatten (assuming prop
                                holds).

                              Phase 6: Byte and u32 operations

                              • Specs for u8_add, u8_xor, u8_shift_left, u8_bit_decomposition, etc.
                              • Specs for u32_less_than.
                              • These require modeling the Goldilocks field arithmetic precisely.

                              Relation to CSLib

                              The approach is analogous to how CSLib provides
                              foundational definitions for formalizing computer science in Lean. Where CSLib defines
                              evaluation semantics for lambda calculus and other foundational languages, we define
                              evaluation semantics for Aiur — a domain-specific circuit language. The key parallel
                              is: define a clean denotational/operational semantics first, then prove properties
                              against it, then (optionally) prove that concrete implementations refine the semantics.

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions