Немного о bfc


Brainfuck — очень глупый язык. Там есть лента из 30к ячеек, по байту каждая. Команды bfc это:


  • Передвижение по ленте влево и вправо (символы < и >)
  • Увеличение и уменьшение значения в ячейке (символы + и -)
  • Ввод и вывод текущей ячейки (символы . и ,)
  • И цикл while, который продолжается пока значение в текущей ячейке не ноль. [ и ] это начало и конец цикла соответственно

Программировать на bfc сложно. Но, как известно, любую проблему можно решить добавлением слоя абстракции (кроме проблемы большого количества абстракций).


Начнём, как обычно, с малого


Основные определения


Основные конструкции попросту один в один переносим в хаскель


data Instr = Inc | Dec | Shl | Shr | In | Out | Loop [Instr] deriving (Eq, Show)
--            +     -     <     >     ,    .     [...]

Интерпретатор bfc приводить не буду, их написано огромное множество. Да и написал я его тяп-ляп только для того, чтобы не нужно было из REPL-а каждый раз выходить.


Главной абстракцией над инструкциями будет кодогенератор:


type CodeGen a = Writer [Instr] a

CodeGen это монада

как спасти принцессу на хаскеле


У тебя есть хаскель (и монада)


Ты создаёшь монаду, чтобы энкапсулировать сайд-эффекты убийства дракона. Жители пытаются тебя остановить, но ты говоришь, что всё норм, потому что монада — это просто моноид в категории эндофункторов и не видишь, что...


Тебе нужна помощь


Выглядит страшновато, но по сути CodeGen просто возвращает список инструкций. Если кодогенераторов несколько, то они вызываются по-очереди, а инструкции склеиваются в большой список.


Основа основ в bfc это смещения. Чтобы не писать их вручную, создаём функции rshift и lshift:


rshift :: Int -> CodeGen ()
rshift x | x > 0 = tell $ replicate   x  Shr
         | x < 0 = tell $ replicate (-x) Shl
         | otherwise = return ()

lshift :: Int -> CodeGen ()
lshift x = rshift (-x)

Если смещение положительное, повторяем x раз смещение вправо, если отрицательное, то повторяем -x раз смещение влево, иначе не делаем ничего. Смещение влево на x это то же самое, что смещение вправо на -x


Добавим удобный способ зациклить код:


loop :: CodeGen () -> CodeGen ()
loop body = tell $ [Loop (execWriter body)]

Внутри мы получаем список инструкций body, вставляем их в Loop, и с помощью tell делаем из этого новый CodeGen ()


Напишем инструкцию reset. Она будет обнулять значение в текущей ячейке.
Алгоритм обнуления прост: пока значение не ноль, уменьшаем его. В bfc это записывается так: [-]. А в наших функциях это запишется так:


reset :: CodeGen ()
reset = loop $ tell [Dec]

Осталось построить ещё несколько функций, и можно строить следующий уровень абстракции. А функции которые нам нужны это move-ы. Они будут помогать в перемещении и копировании значения.


Отвлечёмся немного от хаскеля и посмотрим как это делается в bfc. Для того, чтобы перенести значение из текущей ячейки в соседнюю мы пишем [->+<]. Читать этот код следует так:
пока в ячейке что-то есть, отними единицу, перейди в соседнюю ячейку, добавь туда единицу и вернись обратно.
Этот код будет работать не совсем верно, если в соседней ячейке уже что-то есть, поэтому перед началом переноса нужно её обнулить. Точно так же можно переносить сразу в две или вообще N ячеек. Выглядит код аналогично: пока есть значение в ячейке отнимаем единицу, проходим по всем ячейкам в которые мы переносим эту и добавляем туда по единице. После возвращаемся назад.


Сначала напишем простую версию без обнуления:


moveN' :: From Int -> To [Int] -> CodeGen ()
moveN' (From src) (To dsts) = do
  rshift src -- Переходим к ячейке-источнику
  loop $ do
    tell $ [Dec] -- Однимаем единицу
    lshift src   -- Возвращаемся к базовому адресу

    -- Проходимся по всем ячейкам и добавляем туда единицу
    forM_ dsts $ \dst -> do
      rshift dst   -- Смещение
      tell $ [Inc] -- Инкремент
      lshift dst   -- Возвращение к базовому адресу

    rshift src -- Возвращение к ячейке-источнику

  lshift src -- Возвращение к базовому адресу

А теперь на её основе более сложную:


moveN :: From Int -> To [Int] -> CodeGen ()
moveN (From src) (To dsts) = do
  -- Обнуляем dst ячейки
  forM_ dsts $ \dst -> do
    rshift dst
    reset
    lshift dst

  moveN' (From src) (To dsts)

From и To, которые использованы выше, ничего, по сути, не делают. Это просто слова-обёртки, чтобы не путаться откуда и куда мы всё передаём. Из-за них нужно писать move (From a) (To b), вместо move a b. Первое лично мне понятнее, поэтому я буду придерживаться такого стиля.


Добавим move-ы, которые будут использоваться чаще всего, в отдельные функции, чтобы меньше писать


move :: From Int -> To Int -> CodeGen ()  
move (From src) (To dst) = moveN (From src) (To [dst])

move2 :: From Int -> To Int -> To Int -> CodeGen ()  
move2 (From src) (To dst1) (To dst2) = moveN (From src) (To [dst1, dst2])

Почти везде будет использоваться безопасная (нештрихованная) версия.


Из грязи в князи, от ячеек к регистрам


Как в настоящей машине Тьюринга, заведём пишущую головку, которая будет способна перемещаться по ленте. Почти как та, что есть в bfc из коробки. Но! Эта пишущая головка будет иметь состояние. В ней будет хранится какое-то количество регистров, которые не будут терять своё состояние при перемещении.


Как я её представляю

Пишущая машинка


Определим буфера, регистры общего назначения и временные регистры.


data Register = BackBuf | GP Int | T Int | FrontBuf deriving (Eq, Show)

gpRegisters :: Int
gpRegisters = 16

tmpRegisters :: Int
tmpRegisters = 16

Вместе с ней определяем функцию relativePos, которая на деле синоним к fromEnum. Код приводить не буду, он громоздкий и скучный, но в двух словах: за ноль берётся регистр GP 0, BackBuf имеет позицию -1, после шестнадцати GP регистров идут 16 T регистров, а после них FrontBuf.
После каждой ассемблерной инструкции мы будем возвращаться в позицию GP 0, чтобы относительные адреса никогда не менялись.


inc и dec для регистров


Первое и самое простое, что можно сделать с регистрами это научиться их увеличивать и уменьшать на единицу. Для этого находим положение регистра, идём туда, увеличиваем ячейку на один и возвращаемся в GP 0.


withShift :: Int -> CodeGen () -> CodeGen ()
withShift shift body = do
  rshift shift
  body
  lshift shift

inc :: Register -> CodeGen ()
inc reg = do
  let pos = relativePos reg
  withShift pos $ tell [Inc]

dec :: Register -> CodeGen ()
dec reg = do
  let pos = relativePos reg
  withShift pos $ tell [Dec]

Аналогично делаем определяем ввод и вывод. Соответствующие функции названы inp и out.


Загрузка константы в регистр


Код почти такой же: смещаемся, обнуляем регистр, увеличиваем до нужного значения, возвращаемся к базовому адресу


set :: Register -> Int -> CodeGen ()
set reg val = do
  let pos = relativePos reg

  withShift pos $ do
    reset
    tell (replicate val Inc)

Заодно определяем красивый синоним


($=) :: Register -> Int -> CodeGen ()
($=) = set

Теперь можно писать GP 0 $= 10


Ассемлерная инструкция mov


mov — это первая высокоуровневая инструкция, в которой придётся использовать временные регистры. По умолчанию всегда будем брать первый не занятый регистр. В нашем случае это T 0
Алгоритм переноса такой: сначала переносим значение из регистра x в y и T 0. Потом из T 0 возвращаем значение назад в x, так как move2 испортил значение в x


mov :: From Register -> To Register -> CodeGen ()
mov (From x) (To y) = do
  let src = relativePos x
      dst = relativePos y
      buf = relativePos (T 0)

  move2 (From src) (To dst) (To buf)
  move (From buf) (To src)

Да, mov с регистром T 0 работать не будет. Такой особый временный регистр, в который даже mov-нуть ничего нельзя.


Сложение и вычитание регистров


Привожу только сложение, так как вычитание аналогично:


add :: Register -> To Register -> CodeGen ()
add x (To y) = do
  let src = relativePos x
      dst = relativePos y
      buf = relativePos $ T 0

  -- Переходим к регистру x, чтобы цикл работал правильно
  withShift src $ do
    loop $ do
      tell [Dec] -- Отнимаем единицу
      withShift (-src) $ do -- Относительно базового адреса делаем:
        withShift dst $ tell [Inc] -- Прибавляем 1 к регистру y
        withShift buf $ tell [Inc] -- Прибавляем 1 к буферу

  move (From buf) (To src) -- Переносим буфер обратно в x

В sub изменено одно единственное слово: Inc заменено на Dec в строчке "Прибавляем 1 к регистру y"


Циклы и ветвления


Цикл while принимает на вход регистр и повторяет действия пока в этом регистре не окажется ноль. Для этого нам необходимо, чтобы когда мы начинаем цикл, мы были в ячейке данного регистра, поэтому смещаемся на pos. Но ассемблерные инструкции (тело цикла) требует, чтобы мы всегда были в ячейке GP 0, поэтому в начале цикла смещаемся назад и только потом вызываем само тело цикла.


while :: Register -> CodeGen () -> CodeGen ()
while reg body = do
  let pos = relativePos reg

  withShift pos $ 
    loop $ withShift (-pos) $ body

Ветвление это, можно сказать, цикл, в который мы заходим не более одного раза. В конце цикла уносим регистр в буфер, цикл завершается, так как в регистре ноль, а значение мы возвращаем назад.


when :: Register -> CodeGen () -> CodeGen ()
when reg body = do
  let pos = relativePos reg
      buf = relativePos (T 0)

  while reg $ do
    body
    move (From pos) (To buf)

  move (From buf) (To pos)

Работа с лентой


Пришло время научить нашу машину работать с памятью. Регистров у нас, конечно, много но одними ими сыт не будешь. Первое что нужно научиться делать — загружать и выгружать значения из регистров, так как на их основе будет работать доступ к произвольной ячейке в памяти


Загрузка и выгрузка значений


Загрузка и разгрузка будут происходить в ячейку сразу после переднего буфера


Выглядит это так

Сюда


Нового тут мало, так как это буквально тот же код, что был в mov-ах, с тем исключением, что один "регистр" (ячейка перед пишущей головкой регистром не является) фиксирован


load :: Register -> CodeGen ()
load reg = do
  let dst = relativePos reg
      src = relativePos FrontBuf + 1
      buf = relativePos (T 0)

  move2 (From src) (To dst) (To buf)
  move (From buf) (To src)

store :: Register -> CodeGen ()
store reg = do
  let src = relativePos reg
      dst = relativePos FrontBuf + 1
      buf = relativePos (T 0)

  move2 (From src) (To dst) (To buf)
  move (From buf) (To src)

Мини-босс: Смещение пишущей головки


Сначала переходим к самому правому регистру. Это последний из T-регистров или первый перед FrontBuf. Смещаем регистры на один вправо.


После смещения

Пишущая головка нарисована относительно регистра GP 0.
image


При этом происходит коллизия FrontBuf и ячейки перед машинкой, поэтому перемещаем её в пустое место перед BackBuf


shrCaret :: CodeGen ()
shrCaret = do
  let buf = relativePos FrontBuf
      buf2 = relativePos BackBuf
  rshift buf
  replicateM (gpRegisters + tmpRegisters) $ do
    move (From $ -1) (To 0)
    lshift 1

  rshift 1
  move (From buf) (To $ buf2 - 1)

Смещение влево аналогично, код приводить не буду


После разрешения коллизии

image


Произвольный доступ к памяти


Фух, ну наконец-то, добрались до этой части. Это последняя принципиальная абстракция, скоро перейдём к примерам


derefRead :: From (Ptr Register) -> To Register -> CodeGen ()
derefRead (From (Ptr ptr)) (To reg) = do
  -- Используем T 1, так как T 0 будет испорчен mov инструкцией
  let counter = T 1

  -- Путешествие до участка памяти
  mov (From ptr) (To counter)
  while counter $ do
    shrCaret
    dec counter
  -- Сохраняем на сколько мы ушли, вдруг ptr и reg совпадают
  -- Тогда load потеряет данные
  mov (From ptr) (To counter)

  load reg
  -- Путешествуем назад
  while counter $ do
    shlCaret
    dec counter

derefWrite работает абсолютно аналогично.
Тут, как и с move я ввёл обёртку Ptr, чтобы внести ясность


Примеры


Числа фибоначчи


Посчитаем пятое число фибоначчи


program :: CodeGen ()
program = do
  -- Чтобы BackBuf не выползал за начало ленты (его позиция -- -1)
  -- Смещаемся на 1 вправо
  initCaret

  -- Определяем говорящие синонимы для регистров
  let counter = GP 0
      prev    = GP 1
      next    = GP 2
      tmp     = T 1

  counter $= 5 -- Делаем цикл пять раз
  prev $= 0
  next $= 1

  -- Классический алгоритм для подсчёта чисел Фибоначчи
  while counter $ do
    dec counter

    mov (From next) (To tmp)
    add prev (To next)
    mov (From tmp) (To prev)

  -- Чтобы вывести символ, нужно чтобы он был в каком-то регистре
  -- Выводить будем в одинарной системе счисления, потому что так проще
  let char = tmp
  char $= fromEnum '1'

  while prev $ do
    dec prev
    out char

  -- Перенос строки для красоты
  char $= fromEnum '\n'
  out char

Состояние после запуска

image


Скомпилированный код на bfc
>[-]+>[-]+++++<>[<>>>>>>[-]<<<<<<>>>>>>>>[-]<<<<<<<<[->>>>>>+<<<<<<>>>>>>>>+<<<<<<<<][-]>>>>>>>>[-<<<<<<<<+>>>>>>>>]<<<<<<<<>>>>>>>[-]<<<<<<<>>>>>>[<<<<<<>>>>>>-<<<<<<>>>>>>>>[-]<<<<<<<<<[-]>>[-<>>>>>>>>+<<<<<<<<<+>>]<>[-]>>>>>>>[-<<<<<<<+>>>>>>>]<<<<<<<<<[->>>>>>>>+<<<<<<<<]>>>>>>>]<<<<<<[-]>>>>>>>>[-]<<<<<<<<>>>>>>>[-<<<<<<<+>>>>>>>>+<<<<<<<<>>>>>>>]<<<<<<<>>>>>>>[-]>[-<+>]<<<<<<<<>-<>]<>>>>>[-]<<<<<[>>[-]<<>>>>>>>>[-]<<<<<<<<[->>+<<>>>>>>>>+<<<<<<<<][-]>>>>>>>>[-<<<<<<<<+>>>>>>>>]<<<<<<<<>[-]+<[->>>>>>>>[-]<<<<<<<[->>>>>>>+<<<<<<<]<>+<>>>>>>>>[<<<<<<<<>[-]<>>>>>>>>[-]]<<<<<<<<]>[<>>>>>>>[-]++++++++++++++++++++++++++++++++++++++++++++++++<<<<<<<>>>>>>>>[-]<<<<<<<[->>>>>>>+<<<<<<<]<>]<>[-]>>>>>>>[-<<<<<<<+>>>>>>>]<<<<<<<<>>>>>>>>[-]<<<<<<<[->>>>>>>+<<<<<<<]<>+<>>>>>>>>[<<<<<<<<>[-]<>>>>>>>>[-]]<<<<<<<<>[<>>>>>>>[-]+++++++++++++++++++++++++++++++++++++++++++++++++<<<<<<<>>-<<>>>>>>>>[-]<<<<<<<[->>>>>>>+<<<<<<<]<>]<>[-]>>>>>>>[-<<<<<<<+>>>>>>>]<<<<<<<<>>>>>>>>[-]<<<<<<<<>>>>>>>>>[-]<<<<<<<<<>>>>>>>[-<<<<<<<>>>>>>>>+<<<<<<<<>>>>>>>>>+<<<<<<<<<>>>>>>>]<<<<<<<>>>>>>>[-]>[-<+>]<<<<<<<<>>>>>>>>[-]<[->+<]<<<<<<<>>>>>>>[-]<[->+<]<<<<<<>>>>>>[-]<[->+<]<<<<<>>>>>[-]<[->+<]<<<<>>>>[-]<[->+<]<<<>>>[-]<[->+<]<<>>[-]<[->+<]<>[-]<[->+<]<[-]>>>>>>>>>>[-<<<<<<<<<<+>>>>>>>>>>]<<<<<<<<<>>>>>>+<<<<<>>[<<>>-<<>>-<<+>>]<<]>>>>>[<<<<<>>>>>-<<<<<<[-]>[-<+>][-]>[-<+>]<>[-]>[-<+>]<<>>[-]>[-<+>]<<<>>>[-]>[-<+>]<<<<>>>>[-]>[-<+>]<<<<<>>>>>[-]>[-<+>]<<<<<<>>>>>>[-]>[-<+>]<<<<<<<>>>>>>>>[-]<<<<<<<<<<[->>>>>>>>>>+<<<<<<<<<<]>><>>>>>>>>[-]<<<<<<<<[-]>>>>>>>>>[-<<<<<<<<<>>>>>>>>+<<<<<<<<+>>>>>>>>>]<<<<<<<<<>>>>>>>>>[-]<[->+<]<<<<<<<<.>>>>>]<<<<<

Ваш хаскель только для факториалов и годится


Чистая правда. Напишем свой факториал


mul :: Register -> To Register -> CodeGen ()
mul x (To y) = do
  -- Т 0 уже занят mov и add 
  let acc = T 1
      counter = T 2

  acc $= 0
  mov (From y) (To counter)

  -- Умножение это сложение, просто много раз
  -- y раз к acc прибавляем x 
  while counter $ do
    add x (To acc)
    dec counter

  -- Сохраняем результат
  mov (From acc) (To y)

program :: CodeGen ()
program = do
  let n   = GP 0
      acc = GP 1

  n $= 5
  acc $= 1

  while n $ do
    mul n (To acc)
    dec n

  let char = T 1
  char $= fromEnum '1'

  while acc $ do
    dec acc
    out char

  char $= fromEnum '\n'
  out char

Состояние после запуска

image


Скомпилированный код на bfc
[-]+++++>[-]+<[>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>[-<>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>]<>[-]<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>+<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<<[->>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<][-]>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>-<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<>[-]<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<>+<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<-]>>>>>>>>>>>>>>>>>[-]+++++++++++++++++++++++++++++++++++++++++++++++++<<<<<<<<<<<<<<<<<>[<>-<>>>>>>>>>>>>>>>>>.<<<<<<<<<<<<<<<<<>]<>>>>>>>>>>>>>>>>>[-]++++++++++<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>.<<<<<<<<<<<<<<<<<

А в двоичной можно вывести?


Да легко!


-- Булево НЕ
inv :: Register -> CodeGen ()
inv reg = do
  let tmp = T 1

  mov (From reg) (To tmp)
  reg $= 1
  when tmp $ do
    reg $= 0

-- Работа с лентой как со стеком
push reg = do
  store reg
  shrCaret

pop reg = do
  shlCaret
  load reg

program :: CodeGen ()
program = do
  initCaret 
  let number = GP 0
      digits = GP 1
      char = T 2

  number $= 64 
  digits $= 0 -- Сохраняем количество цифр, чтобы не провалить стек

  while number $ do
    let tmp    = T 3
        isEven = T 4

    -- Находим чётность числа
    isEven $= 1

    mov (From number) (To tmp)    
    while tmp $ do
      inv isEven
      dec tmp

    -- Если делится на два 
    when isEven $ do
      char $= fromEnum '0'

    -- Если не делится на два
    inv isEven
    when isEven $ do
      char $= fromEnum '1'
      -- Делаем так, чтобы делилось
      dec number

    -- Записываем цифру в стек
    push char
    inc digits

    -- Делим число на два
    mov (From number) (To tmp)
    number $= 0
    while tmp $ do
      tmp -= 2
      number += 1

  -- Выводим по одной цифре
  while digits $ do
    pop char
    out char
    dec digits
  -- Ну и перенос строки
  char $= fromEnum '\n'
  out char

Состояние после запуска

image


Скомпилированный код на bfc
>[-]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++>[-]<[>>>>>>>>>>>>>>>>>>>>[-]+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<[->>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<][-]>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>-<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>[-]++++++++++++++++++++++++++++++++++++++++++++++++<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>[-]+++++++++++++++++++++++++++++++++++++++++++++++++<<<<<<<<<<<<<<<<<<->>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><[-]<[->+<]><><<[-]>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>+<>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<[->>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<][-]>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<[-]>>>>>>>>>>>>>>>>>>>[<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>--<<<<<<<<<<<<<<<<<<<+>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<]>[<<[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<>[-]>[-<+>]<><<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<[->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<]>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>[-]<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>[-<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>+<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>]<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>.<<<<<<<<<<<<<<<<<<>-<>]<>>>>>>>>>>>>>>>>>>[-]++++++++++<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>.<<<<<<<<<<<<<<<<<<

UPD: Градиентная картинка


К bfc достаточно легко прикрутить вывод в файлик. Netpbm очень простой файловый формат. Попробуем сгенерировать что-то:


-- вывод текста
bprint :: String -> CodeGen ()
bprint str = do
  let tmp = T 0
  forM_ str $ \char -> do
    tmp $= fromEnum char
    out tmp

program :: CodeGen ()
program = do
  initCaret
  -- Заголовок файла
  bprint "P6\n"
  -- Размер картинки 255x255
  bprint "255 255\n"
  -- Максимальное значение цвета
  bprint "255\n"

  c $= 127
  a $= 255
  -- Двойной цикл по картинке
  while a $ do
    b $= 255
    while b $ do
      out a -- Red
      out b -- Green
      out c -- Blue
      dec b
    dec a

Вывод у програмки бинарный, поэтому показывать его не буду


Сгенерированная картинка

image


Скомпилированный код на bfc
>>>>>>>>>>>>>>>>>[-]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.--------------------------.--------------------------------------------.----------[-]++++++++++++++++++++++++++++++++++++++++++++++++++.+++..---------------------.++++++++++++++++++.+++..-------------------------------------------.----------[-]++++++++++++++++++++++++++++++++++++++++++++++++++.+++..-------------------------------------------.----------<<<<<<<<<<<<<<[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++<<[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[>[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<.>.>.<-]<-]


Ну вот, статья подошла к концу. Время заветной картинки


image

Комментарии (17)


  1. orenty7 Автор
    02.06.2023 20:21
    +1

    Кстати, весь исходный код можно было бы хорошо сжать. Кажесь, там большую часть кода занимают смещения по регистрам. У меня их всего 32, но можно было бы обойтись восьмью: четыре общего назначения, четыре временных. Это бы сделало код в несколько раз короче


    1. iassasin
      02.06.2023 20:21
      +2

      Можно и без сокращения числа регистров сократить код. Набросал простенький оптимизатор на js :)

      let program = '>>><<<<+<<>>>';
      let oldp, newp = program;
      do { oldp = newp; newp = oldp.replace(/><|<>/g, ''); } while (oldp != newp);
      console.log(newp); // <+>
      


      1. orenty7 Автор
        02.06.2023 20:21
        +1

        Круть! Написал похожий оптимизатор на хаскеле, добавил схлопывание плюсов с минусами, немного модифицировал кодогенераторы, чтобы программы были подогнаны под оптимизатор и скомпилированный код стал в разы короче


  1. Sild
    02.06.2023 20:21
    +4

    Ох, эта красота заставила улыбнуться

    И в целом хороший туториал по теме «как и желудей и веток получились процессоры»


  1. radiolok
    02.06.2023 20:21
    +5

    Годнота! итоговый код на brainfuck выглядит сильно оптимальнее того что генерирует C2bf - там такая лютая стек-машина генерируется, что производительность падает в сотни раз по сравнению с нативым кодом на bf.

    Возьму себе на вооружение - буду программки для лампового компьютера генерировать. Осталось только выучить хаскель :)


    1. forthuse
      02.06.2023 20:21

      Стековая машина присутствует в дизайне эзотерического языка Befunge и его вариантов.
      тоже есть где "развернуться" :)


  1. KongEnGe
    02.06.2023 20:21

    Продвинутые эйчары еще не начали на литкоде предлагать быстренько порешать задачи на bf?


  1. tongohiti
    02.06.2023 20:21

    Давно буханки на хабре не было..


    1. thevlad
      02.06.2023 20:21
      +4

      Ну это не совсем буханка, подобные техники использовались в локальных DRM защитах, когда код ее ядра компилировался в "виртуальную машину" которая представляла из себя лютый трэш.


      1. tongohiti
        02.06.2023 20:21
        +1

        Я про картинку в конце статьи.


  1. DrMefistO
    02.06.2023 20:21

    Я конечно нуб в BF, но имея только указанные операции, как присваивается начальное значение для того, что будет изменяться?


    1. orenty7 Автор
      02.06.2023 20:21
      +2

      [-]++...+. В си это эквивалентно

      while(*ptr) {
          *ptr --;
      }
      // Тут *ptr == 0 
      *ptr ++;
      *ptr ++;
      // ...
      *ptr ++;
      // Тут у *ptr нужное значение


      1. DrMefistO
        02.06.2023 20:21

        Спасибо. Стало понятнее.


        1. AbnormalHead
          02.06.2023 20:21
          +1

          В качестве дополнительного комментария. Первые три символа - это очищение ячейки (можно и опускать, но никто не гарантирует, что там раньше был 0), потом плюсиками увеличиваем значение ячейки до нужного значения. И вот здесь можно применить небольшую хитрость.

          например надо в ячейку записать число 27.

          решение в лоб - вбиваем 27 плюсов.

          +++++++++++++++++++++++++++

          решение два. представим число 27 как 5*5+2 на bf это будет выглядеть так:

          >[-]+++++[<+++++>-]<++

          прыгаем на следующую ячейку, заносим туда число 5. возвращаемся на текущую ячейку. увеличиваем ее на 5, прыгаем на следующую ячейку, уменьшаем ее на 1 и если не ноль, то возвращаемся на знак "[" и повторяем цикл. получаем 5*5. осталось только добавить 2.

          В результате получили сокращение кода. И чем больше число, тем больше будет сокращение.

          Для автора, это кстати, идея для организации дальнейшей оптимизации получаемого кода.


          1. orenty7 Автор
            02.06.2023 20:21

            Можно несколько констант задавать одновременно и таким образом сжать код ещё сильнее.

            В bfc простор для оптимизаций просто огромный, но я все нетривиальные оптимизации отметаю в угоду универсальности кода, так как хочу замахнуться ещё на уровень выше



      1. AbnormalHead
        02.06.2023 20:21

        Промахнулся.