Zulip Chat Archive

Stream: new members

Topic: operational semantics for brainfuck


Stefan (Jan 07 2024 at 00:58):

Hi!!! So I'm trying to define the operational semantics of Brainfuck, focusing on small step semantics at first, and I'm stuck on defining the core element of the state, which is a memory cell. This memory cell would be the equivalent of a byte, so a natural number strictly smaller than 256. How would I go about defining this in order to later be able to reason about it?

Matt Diamond (Jan 07 2024 at 01:11):

Fin 256, perhaps?

Matt Diamond (Jan 07 2024 at 01:15):

also consider docs#Std.BitVec

Kyle Miller (Jan 07 2024 at 01:21):

docs#UInt8 is Fin 256 but has compiler support where the runtime representation is a byte.

Stefan (Jan 07 2024 at 22:56):

Thank you for the suggestions!! I'll probably get stuck again and come back

Stefan (Jan 10 2024 at 16:43):

Hello! While proving a theorem using BigStep semantics I ran into the following case:

case mp
s t : State
h : BigStep (Op.pDec, s) t
 t = State.pDec s

this can be proved with

cases h
. rfl

but it is really not clear to me why we can use cases here.
after using cases the goal and assumptions change to

case mp.pDec
s : State
 State.pDec s = State.pDec s

and I can't really wrap my head around this.

For reference State is defined as follows:

structure BFBand : Type where
  before: List Nat
  current: Nat
  after: List Nat
  deriving Repr

structure State : Type where
  input: List Nat
  output: List String
  band: BFBand
  deriving Repr

pDec is defined as

namespace State

def pDec (s: State): State :=
  let b := s.band
  let newBand :=
    match b.before with
      | [] =>  ⟨[], 0, b.current :: b.after
      | h :: t => t, h, b.current :: b.after
  s.input, s.output, newBand

end State

Op is defined as

inductive Op : Type where
  | nop        : Op
  | pInc       : Op
  | pDec       : Op
  | vInc       : Op
  | vDec       : Op
  | brakPair   : Op -> Op
  | seq        : Op -> Op -> Op
  | output     : Op
  | input      : Op

and BigStep.pDec is defined as:

inductive BigStep: Op × State  State  Prop where
| pDec (s: State): BigStep (Op.pDec, s) s.pDec

Luigi Massacci (Jan 10 2024 at 16:54):

Ignoring for a moment whether you have the right definitions., you can use case because BigStep is an inductive type, and case will essentially be “induction without the inductive hypothesis” (i.e. show that your proposition holds for any possible constructor of your inductive type). You have only one constructor, so Lean reasonably substitutes that in, and t is forced to be s.pDec because that is the only thing that it can be, according to your definition of BigStep.

Stefan (Jan 10 2024 at 17:02):

I can kind of get behind that, but I only gave the partial definition of BigStep to save some space. Here is the full definition for more context. Why is cases not trying to match the other constructors? Is it immediately obvious that only the pDec constructor can be matched?

inductive BigStep: Op × State  State  Prop where
  | nop  (s: State): BigStep (Op.nop, s)  s
  | pInc (s: State): BigStep (Op.pInc, s) s.pInc
  | pDec (s: State): BigStep (Op.pDec, s) s.pDec
  | vInc s:
      BigStep (Op.vInc, s) (
        let b := s.band
        s.input, s.output, b.before, b.current + 1, b.after⟩⟩)
  | vDec s:
      BigStep (Op.vDec, s) (
        let b := s.band
        s.input, s.output, b.before, b.current - 1, b.after⟩⟩)
  | brakPairTrue {ops} {s t u: State} (c: s.dereference  0)
    (body: BigStep (ops, s) t)
    (rest: BigStep ((Op.brakPair ops), t) u):
      BigStep (Op.brakPair ops, s) u
  | brakPairFalse ops (s: State) (c: s.dereference = 0):
      BigStep (Op.brakPair ops, s) s
  | seq (ops1 s ops2 t u)
    (h:  BigStep (ops1, s) t)
    (h': BigStep (ops2, t) u):
      BigStep ((Op.seq ops1 ops2), s) u
  | input s:
      BigStep (Op.input, s) (
        let b := s.band
        match s.input with
          | [] => ⟨[],
                   "<<error occurred: Not enough input>>" :: s.output,
                   b.before, 0, b.after⟩⟩
          | h :: t => t, s.output, b.before, h, b.after⟩⟩)
  | output s:
      BigStep (Op.output, s) (
        let b := s.band
        let c := (Char.ofNat b.current).toString
        s.input, c :: s.output, b⟩)

Stefan (Jan 14 2024 at 13:56):

Hi again! I have the big step semantics defined for my language and I'm trying to write a proof for a swap operation. More concretely, if the sequence of operations is OPS and the initial state is s := {[], 0, [x, y]}, then (OPS, s) ==> {[], 0, [y, x]}. Unfortunately I'm stuck midway through, proving the following lemma:

(OPS', { [0], k, [] }) ==> { [k], 0, [] } -> (OPS', { [1], k, [] }) ==> { [1 + k], 0, [] }

here OPS' is the form (Op.brakPair ops), which would be the equivalent of while (s.current != 0) { ops } in a C-like language and s.current is the non-list field of a state.

I don't really have any idea on how to approach this.

Mario Carneiro (Jan 14 2024 at 14:02):

I don't see any reason why that implication would be true

Mario Carneiro (Jan 14 2024 at 14:03):

It seems like you have the wrong inductive hypothesis

Mario Carneiro (Jan 14 2024 at 14:03):

Do you have a theorem statement?

Stefan (Jan 14 2024 at 14:24):

I suspect I wasn't clear enough in my explanation. OPS are not generic operations, but a particular set that should indeed swap two numbers. I also edited the previous message with a clarification changing OPS to OPS' in the lemma. The implication should now make more sense. Also, here is the full theorem statement:

def bfSwapTX: Op := >_[<_+_>_-]
def bfSwapXY: Op := >_[<_+_>_-]
def bfSwapYT: Op := <_<_[>_>_+_<_<_-]
def bfSwap: Op := (bfSwapTX)_(bfSwapXY)_(bfSwapYT)

theorem swap {x y: Nat}: (bfSwap, (State.mk [] 0 [x, y]))  State.mk [] 0 [y, x] := sorry

Mario Carneiro (Jan 14 2024 at 14:34):

from what I can tell the inductive hypotheses for the three loops are:

([<_+_>_-], (State.mk (a :: l) b r))  State.mk (a+b :: l) 0 r -- loops bfSwapTX and bfSwapXY
([>_>_+_<_<_-], (State.mk l a (y :: b :: r)))  State.mk l 0 (y :: a+b :: r) -- bfSwapYT

Mario Carneiro (Jan 14 2024 at 14:35):

you prove the first one by induction on b generalizing a, and the second one by induction on a generalizing b

Mario Carneiro (Jan 14 2024 at 14:35):

and the rest is just forward simulation

Stefan (Jan 14 2024 at 14:54):

You are correct. Where I get stuck is on the successor step in the induction:

case succ
l r : List Nat
a k : Nat
t : (Op.brakPair (<_+_>_-), { before := a :: l, current := k, after := r } )  { before := (a + k) :: l, current := 0, after := r }
 (Op.brakPair (<_+_>_-), { before := a :: l, current := Nat.succ k, after := r })   { before := (a + Nat.succ k) :: l, current := 0, after := r }

It's probably trivial, but I don't know how to progress with the proof from this step onwards

Mario Carneiro (Jan 14 2024 at 14:54):

you should apply brakPairTrue there

Mario Carneiro (Jan 14 2024 at 14:55):

you forgot to generalizing a there though

Mario Carneiro (Jan 14 2024 at 14:55):

because you need to apply the inductive hypothesis with a+1 in place of a

Stefan (Jan 14 2024 at 15:03):

Thank you very much for the input!! Could you guide to where I could see some examples using generalizing and/or some info on how to use it? This is new to me

Mario Carneiro (Jan 14 2024 at 15:15):

induction b generalizing a

Stefan (Jan 14 2024 at 18:08):

I got it, thank you very much for the help!

Stefan (Jan 14 2024 at 21:46):

I'm now trying to parse a string into an Op, but can't figure how to prove termination

Op is defined as follows:

inductive Op : Type where
  | nop        : Op
  | pInc       : Op
  | pDec       : Op
  | vInc       : Op
  | vDec       : Op
  | brakPair   : Op -> Op
  | seq        : Op -> Op -> Op
  | output     : Op
  | input      : Op

so far I've tried:

def fromString (s: String): Op :=
  parse (s.toUTF8.toList)
where
  parse (chrl: List UInt8): Op :=
    match chrl with
    | [] => nop
    | h :: t =>
      match h with
      | 62 => Op.seq Op.pInc   (parse t)        -- 62 >
      | 60 => Op.seq Op.pDec   (parse t)        -- 60 <
      | 43 => Op.seq Op.vInc   (parse t)        -- 43 +
      | 45 => Op.seq Op.vDec   (parse t)        -- 45 -
      | 46 => Op.seq Op.output (parse t)        -- 46 .
      | 59 => Op.seq Op.input  (parse t)        -- 59 ;
      | 91 =>                                   -- 91 [
        let head := t.takeWhile (λ x => x  93) -- 93 ]
        let tail := t.dropWhile (λ x => x  93) -- 93 ]
        let body := parse head
        let rest := parse tail
        Op.seq (Op.brakPair body) rest
      | _ => parse t -- anything else skip
    termination_by
      fromString s => s
      parse chrl => chrl

termination fails to be proven body and rest

Joachim Breitner (Jan 15 2024 at 09:41):

Can you, in prose, say why you think parse is terminating?

Stefan (Jan 15 2024 at 13:28):

Yes. With every recursive call the passed char list will be at least one character smaller in size. in the 91 case, len(head) + len(tail) = len(t), so again, the amount of chars processed will decrease. Execution stops when an empty list is reached.

Joachim Breitner (Jan 15 2024 at 13:37):

Ok, but that is a sizeable amount of arguing, and beyond what lean can do by default for you these days.

The first thing you need to do is to tell lean that you expect the length of the list to be decreasing (and no the size, which takes the “size” of the charaters into account):

      parse chrl => chrl.length

Joachim Breitner (Jan 15 2024 at 13:44):

Now the error message will be

failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
h: UInt8
t: List UInt8
head: List UInt8 := List.takeWhile (fun x => decide (x  93)) t
 List.length (List.takeWhile (fun x => !decide (x = 93)) t) < Nat.succ (List.length t)

Stefan (Jan 15 2024 at 13:44):

After trying that the error persists and I get the following message:

failed to prove termination, possible solutions:
  - Use `have`-expressions to prove the remaining goals
  - Use `termination_by` to specify a different well-founded relation
  - Use `decreasing_by` to specify your own tactic for discharging this kind of goal
h : UInt8
t : List UInt8
head : List UInt8
 List.length (List.takeWhile (fun x => !decide (x = 93)) t) < Nat.succ (List.length t)

If there is no other option, I would attempt prooving what's suggested, but don't understand what the exact syntax for beginning the poof should be. However, if you have other ideas to share, I would very much appreciate it

Joachim Breitner (Jan 15 2024 at 13:46):

Normally it should work to prove the relevant relations right in the code, e.g.

      | 91 =>                                   -- 91 [
        let head := t.takeWhile (λ x => x  93) -- 93 ]
        have h1 : head.length < t.length.succ := sorry
        let tail := t.dropWhile (λ x => x  93) -- 93 ]
        have h2 : tail.length < t.length.succ := sorry
        let body := parse head
        let rest := parse tail
        Op.seq (Op.brakPair body) rest

But in this case, there is some annoyance that (λ x => x ≠ 93) gets rewritten to (fun x => !decide (x = 93)) and and now assumption doesn’t work. You can work around it by writing

    decreasing_by first | decreasing_tactic | simp_wf; simp only [ne_eq, decide_not] at *; assumption

(IMHO this is a bug, the simp_wf in the default decreasing_tactic should only unfold wf stuff but otherwise leave the goal as it as far as possible).

Stefan (Jan 15 2024 at 15:08):

Thank you very much!

Stefan (Jan 15 2024 at 15:09):

I managed to finalize the proofs


Last updated: May 02 2025 at 03:31 UTC