import ApcOptimizer.Spec
import ApcOptimizer.MemoryBus

set_option autoImplicit false

namespace ApcOptimizer.OpenVM

variable {p : ℕ}

/-- The OpenVM bus types that appear in the default bus map. -/
inductive OpenVmBusType where
  /-- A instruction should receive a `(pc, timestamp)` and send updated values to the bus. -/
  | executionBridge
  /-- To access a memory cell, instructions should:
      1. Receive a `(address_space, pointer, data... (4 bytes), prev_timestamp)` tuple
      2. Send a `(address_space, pointer, data... (4 bytes), current_timestamp)` tuple. -/
  | memory
  /-- Lookup to get the instruction flags from the current pc. Format: `(pc, flags...)` -/
  | pcLookup
  /-- Check that a value is in a certain range. Format: `(value, bits)` -/
  | variableRangeChecker
  /-- Check that a value is in a certain range. Format: `(x, y, z, op)` where `op` is either
      0 (range check) or 1 (xor). The bus checks that `x` and `y` are bytes, and either
      `op = 0 ∧ z = 0` (range check) or `op = 1 ∧ z = x ^ y` (xor). -/
  | bitwiseLookup
  /-- Check that a tuple of two values is in a certain range. Format: `(x, y)` where
      `x < size1` and `y < size2`. -/
  | tupleRangeChecker (size1 size2 : Nat)
  deriving Repr, DecidableEq

/-- A mapping from bus IDs to bus type. -/
abbrev BusMap := Nat → Option OpenVmBusType

/-- A concrete bus map as parsed from a powdr export's `bus_map.bus_ids` field:
    an association list bus id ↦ bus type. -/
abbrev BusMapList := List (Nat × OpenVmBusType)

/-- Convert a `BusMapList` to a `BusMap` lookup function. -/
def BusMapList.toBusMap (busMap : BusMapList) : BusMap :=
  fun busId => busMap.lookup busId

/-- The hard-coded default OpenVM bus map, mirroring powdr's `default_openvm_bus_map` -/
def defaultBusMap : BusMap
  | 0 => some .executionBridge
  | 1 => some .memory
  | 2 => some .pcLookup
  | 3 => some .variableRangeChecker
  | 6 => some .bitwiseLookup
  | 7 => some (.tupleRangeChecker 256 2048)
  | _ => none

/-- Stateful buses are the execution bridge and memory; the rest are stateless lookups. -/
def OpenVmBusType.isStateful : OpenVmBusType → Bool
  | .executionBridge => true
  | .memory => true
  | .pcLookup => false
  | .variableRangeChecker => false
  | .bitwiseLookup => false
  | .tupleRangeChecker _ _ => false

/-- Whether a field element is a byte (`0 ≤ x < 256`). -/
def isByte (x : ZMod p) : Bool := decide (x.val < 256)

/-- Whether a message conflicts with the lookup table of the bus it is sent on. -/
def violates (busMap : BusMap) (msg : BusInteraction (ZMod p)) : Bool :=
  match busMap msg.busId, msg.payload with
  -- ISSUE:
  -- The PC lookup is a bit special: We would have to know the program to
  -- check whether the PC lookup is valid. So this semantics is **wrong**,
  -- and in theory, the optimizer could add failing PC lookups, losing
  -- completeness.
  -- However, the input circuit already has constraints fixing all of the
  -- lookup fields to constants (added here: [1]), so the optimizer would
  -- not be able to change any of the flags.
  -- Also, the completeness issue could be solved by checking that the
  -- optimized circuit does not contain any PC lookups (they can always
  -- be removed in practice).
  -- [1] https://github.com/powdr-labs/powdr/blob/f94a24f19249af67efbea92ff9c3db6e3e50e7fd/autoprecompiles/src/symbolic_machine_generator.rs#L185-L192
  | some .pcLookup, args => !decide (args.length = 9)

  | some .bitwiseLookup, [x, y, z, op] =>
    match op.val with
    -- Op 0: range check x and y to be bytes, z must be 0.
    | 0 => !(isByte x && isByte y && decide (z.val = 0))
    -- Op 1: range check x and y to be bytes, z must be x ^ y.
    | 1 => !(isByte x && isByte y && decide (z.val = Nat.xor x.val y.val))
    -- Other ops are invalid.
    | _ => true

  | some .variableRangeChecker, [x, bits] =>
    -- Check that x < 2^bits, and that the number of bits requested is one the checker
    -- supports: OpenVM's variable range checker rejects any lookup of more than 25 bits.
    !(decide (bits.val ≤ 25) && decide (x.val < 2 ^ bits.val))

  | some (.tupleRangeChecker s1 s2), [x, y] =>
    -- Range check that x < s1 and y < s2.
    !(decide (x.val < s1) && decide (y.val < s2))

  -- For lookups, reject if the number of arguments is not as expected.
  | some .bitwiseLookup, _ => true
  | some .variableRangeChecker, _ => true
  | some (.tupleRangeChecker _ _), _ => true

  -- Stateful buses.
  | some .executionBridge, _ => false

  -- In OpenVM, the invariant is that only range-checked values are sent to
  -- the register & memory address spaces.
  | some .memory, addressSpace :: _pointer :: b0 :: b1 :: b2 :: b3 :: _timeStamp =>
    decide (msg.multiplicity = -1) &&
      (addressSpace.val == 1 || addressSpace.val == 2) &&
      !([b0, b1, b2, b3].all isByte)
  | some .memory, _ => false

  -- Invalid bus ID. Won't have a matching receive.
  | none, _ => true

/-- Whether a message breaks an invariant on which soundness depends. -/
def breaksInvariant (busMap : BusMap) (msg : BusInteraction (ZMod p)) : Bool :=
  -- Note that this function is not called for multiplicity = 0
  match busMap msg.busId with
  -- Lookups are only ever sent (multiplicity 1).
  | some .pcLookup | some .variableRangeChecker | some .bitwiseLookup
  | some (.tupleRangeChecker _ _) =>
    !decide (msg.multiplicity = 1)
  -- The execution bridge is stateful: it is sent (1) or received (-1).
  | some .executionBridge =>
    !decide (msg.multiplicity = 1 ∨ msg.multiplicity = -1)
  -- Memory is stateful (multiplicity 1 or -1), and additionally maintains the invariant
  -- that data limbs written to the register / main-memory address spaces (1 and 2) are
  -- byte-range. The payload is `(address_space, pointer, data.. (4 bytes), timestamp)`.
  | some .memory =>
    !decide (msg.multiplicity = 1 ∨ msg.multiplicity = -1) ||
    (match msg.payload with
      | addressSpace :: _pointer :: b0 :: b1 :: b2 :: b3 :: _timeStamp =>
        bif (addressSpace.val == 1 || addressSpace.val == 2) then
          !([b0, b1, b2, b3].all (fun d => decide (d.val < 256)))
        else false
      | _ => false)
  -- Circuits should not send messages to an unknown bus.
  | none => true

/-- Assume that x0 always returns 0. This should be enforced globally by all OpenVM chips. -/
def x0ReturnsZero (busMap : BusMap) (msgs : List (BusInteraction (ZMod p))) : Prop :=
  ∀ m ∈ msgs, busMap m.busId = some .memory →
    -- If address space is 1 (registers), and address is 0 (x0), then the value must be 0.
    m.payload[0]? = some 1 → m.payload[1]? = some 0 →
      m.payload[2]? = some 0 ∧ m.payload[3]? = some 0 ∧
        m.payload[4]? = some 0 ∧ m.payload[5]? = some 0

/-- Maps a bus ID to its memory bus shape, if applicable. -/
def memShapeOf (busMap : BusMap) (busId : Nat) : Option MemoryBusShape :=
  match busMap busId with
  -- The *actual* memory bus, with address (address space, pointer) in payload slots 0 and 1.
  | some .memory => some { addressFields := [0, 1] }
  -- The execution bridge can also be viewed as a memory bus with a single global cell (address `[]`).
  -- Note that in this bus, the memory discipline (for any consecutive send/receive pair, the values
  -- must match) is *not* enforced by the bus itself. By adding the execution bridge here, make
  -- completeness partial: We assume that the prover will always *chose* to prove consecutive cycles.
  | some .executionBridge => some { addressFields := [] }
  | _ => none

/-- The OpenVM bus semantics for a given bus map (default: the hard-coded default bus map). -/
def openVmBusSemantics (p : ℕ) (busMap : BusMap := defaultBusMap) :
    BusSemantics p where
  isStateful busId :=
    match busMap busId with
    | some t => t.isStateful
    | none => false
  violatesConstraint := violates busMap
  breaksInvariant := breaksInvariant busMap
  admissible msgs :=
    (∀ (busId : Nat) (shape : MemoryBusShape), memShapeOf busMap busId = some shape →
      admissibleMemoryBus shape (msgs.filter (fun m => m.busId = busId)))
    ∧ x0ReturnsZero busMap msgs
  -- OpenVM's proving backend bound (powdr's `DEFAULT_DEGREE_BOUND`).
  degreeBound := { identities := 3, busInteractions := 2 }

/-- The BabyBear field modulus, `2^31 - 2^27 + 1` — the field all powdr OpenVM exports use. -/
def babyBear : Nat := 2013265921

instance : NeZero babyBear := ⟨by decide⟩

end ApcOptimizer.OpenVM
