최근 수정 시각 : 2024-09-12 18:33:35

브레인퍽

BrainFuck에서 넘어옴
1. 개요2. 명칭3. 명령어4. 메모리 구조5. 예제
5.1. Hello, world!
5.1.1. 해설
5.2. Hello, world! 노가다 출력
6. 구현의 난점7. 기타

1. 개요

BrainFuck

대표적인 난해한 프로그래밍 언어. 1993년 우어반 뮐러가 제작했으며 제작된 파일 확장자는 .b/.bf.

개발 목적은 가장 작은 컴파일러로 구현할 수 있는 튜링 완전[1] 프로그래밍 언어를 만드는 것이었다고 한다. 세상에서 가장 단순하면서 복잡한 언어라는 말이 어울리는 언어로, 프로그래밍에 사용되는 문자는 +-][><,. 로 딱 8개다.

프로그래밍 방법은 엄청나게 난해하지만, 일단 튜링 완전 타입의 언어이기 때문에 이론적으로는 컴퓨터가 할 수 있는 모든 일을 다 할 수 있다. 그리고 컴퓨터의 연산이 작동하는 방법을 이해하고 있다면, 일단 "배우는" 것은 매우 쉽고 단순하다. 실제로 사용하기가 노가다인데다 매우 난해한 게 문제다. 일단은 일반적인 난해한 프로그래밍 언어들이 다 그렇듯이 포인터를 기본으로 한 명령 체계를 사용하고 있으며 명령어들 역시 포인터를 옮기는 명령어들로 구성되어 있다.

2. 명칭

프로그래밍 언어치곤(?) 굉장히 독특하게 이름이 두 단어 이상으로 이루어진 점과 무엇보다 욕설이 들어간다는 점(...)으로도 유명한데, 원래 존재하던 영어 슬랭 속어 중 하나인 'Brainfuck'에서 유래되었다.[2] 단순히 '강간'[3] 정도로 번역하는 경우도 많지만 원래 뜻은 '정신사납게 하다', '(뇌 용량을 초과할 정도로) 혼란스럽게 하다' 정도의 의미이다. 굳이 번역하면 '머리 터질 것 같은', '정신나갈거같애' 정도에 가깝다. 그야말로 이 언어로 프로그래밍하는 사람들의 뇌를 조져버리겠다는 포부를 갖고 있다.

어쨌거나 이름에 fuck이 들어가기 때문에 웬만해선 욕설 필터에 걸리는 경우가 많다. 그러다보니 보통 돌려서 BrainF*** , 또는 약자만 따서 BF(확장자이기도 하다) 등으로 부르는 경우가 많다. 그런데 구글은 검색 자동완성에선 검열되어 brainf***까지만 자동완성이 뜨지만, 일단 검색하고 결과를 보면 대담하게도 대놓고 위키피디아 링크와 함께 brainfuck이라고 띄우는 것을 볼 수 있다.

3. 명령어

> 포인터 증가
< 포인터 감소
+ 포인터가 가리키는 바이트의 값을 증가
- 포인터가 가리키는 바이트의 값을 감소
. 포인터가 가리키는 바이트 값을 아스키 코드 문자로 출력한다.
, 포인터가 가리키는 바이트에 아스키 코드 값을 입력한다. 쉽게 말해서 입력 받는 역할이다.
[ 포인터가 가리키는 바이트의 값이 0이 되면 짝이 되는 ]로 이동한다. 의사코드로는 while(*ptr != 0) {...} 이다.
] 포인터가 가리키는 바이트의 값이 0이 아니면 짝이 되는 [로 이동한다.
EOF(\\0) 문자로 드러나지 않는 토큰이며 프로그램을 종료한다.

이외에 공백, 엔터 등 기타 문자는 모두 처리되지 않는다.[4]

4. 메모리 구조

1바이트 정수(char)로 이루어진 32768개의 배열과 포인터 하나가 메모리의 전부.
따라서 총 메모리 공간은 32KB + 4B이다.
물론 인터프린터의 구현체별로 당연히 차이가 있다. 한 자바 구현체의 경우는 65535개의 포인터가 있는 대신 4바이트가 아닌 1바이트로 처리한다.

5. 예제

5.1. Hello, world!

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

5.1.1. 해설

이 문단에서는 위 프로그램이 어떻게 동작하여 Hello, World!를 출력하는지를 설명한다. 포인터는 처음에 0번 주소를 가리키고 있으며, 모든 메모리의 초기값은 0임을 잊지 말자. 가독성을 위하여 필요한 부분에는 띄어쓰기를 하였다.
그 전에 잠깐 포인터에 대한 설명을 하자면, 포인터는 메모리의 주소를 가리키는 역할을 한다. 포인터가 가리키는 주소와 실제 그 주소의 값은 별개의 값으로 취급해야 함을 명심하자. 예를 들어 포인터가 5번 주소를 가리키고 있다면, 5번 주소의 값은 123 이런식이다.
  1. +++++ +++++
    포인터가 가리키는 주소(0)의 값을 10 증가시켰다. 메모리의 초기값은 0이므로, 최종적으로 포인터가 가리키는 값에 10이 들어간다.
  2. [>+++++ ++ >+++++ +++++ >+++ >+ <<<<
    포인터가 가리키는 주소(0)를 하나씩 증가시키며( > (1) > (2) > (3) > (4) ) 각 주소의 값을 7, 10, 3, 1씩 증가시킨다. 이후 다시 포인터가 가리키는 주소를 0으로 감소시킨다.
  3. -]
    포인터가 가리키는 주소(0)의 값을 1 감소시킨 후, 그 값이 0이 아닐 경우 2.의 [ 로 돌아간다. 즉 10회 루프문이 되며, 메모리의 1~4번째 주소에는 각각 70, 100, 30, 10의 값이 들어가게 된다.
  4. >++ .H
    포인터가 가리키는 주소를 1 증가시키고(1), 주소의 값(=70)에 2를 더한 후(=72) 그 값을 아스키 코드로 출력한다. 72에 대응되는 아스키 코드 문자는 H이다.
  5. >+ . +++++ ++ .. +++ .ello
    포인터가 가리키는 주소를 1 증가시키고(2), 주소의 값(=100)에 1을 더한 후(=101) 한 번 출력, 7을 더한 후(=108) 두 번 출력, 또다시 3을 더한 후(=111) 한 번 출력한다. 101, 108, 108, 111에 대응되는 아스키 코드 문자는 ello이다.
  6. >++++++++++++++.------------.<<+++++++++++++++.>.+++.------.--------.>+., World!
    4~5와 같다. 포인터가 가리키는 주소를 이리저리 바꾸고 그 주소의 값에 숫자를 더하고 빼 가면서 문자를 출력한다. 여기서 , World!가 출력되는 것.

5.2. Hello, world! 노가다 출력

위와는 다르게 이런 식으로 +를 도배해도 출력이 가능하다.
[-] H
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++ .
[-] e
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++ + .
[-] l 2
>++
[<
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++.
[-]
>-]< o
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ +.
[-]
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++.
[-] Space
++++++++++ ++++++++++ ++++++++++ ++.
[-] w
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ +++++++++.
[-] o
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ +.
[-] r
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++.
[-] l
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++.
[-] d
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++
++++++++++ ++++++++++ ++++++++++ ++++++++++ ++++++++++.
[-] !
++++++++++ ++++++++++ ++++++++++ +++.
[-] Lf
++++++++++.
[-]

6. 구현의 난점

다만 너무 명세가 간단하기 때문에, 상세한 내용에 대해서는 구현체를 만드는 개발자들 몫으로 남겨두었다. 대표적으로 구현체를 제작할 때 고민해야 하는 점들은 다음과 같다.
  • 메모리 셀 크기, 메모리 용량 문제
    보통 셀 하나의 크기는 8bit또는 32bit로 최소 30000개 이상을 가지는 것이 일반적인데, 이는 단순히 우어반 뮐러가 처음 어셈블리로 제작한 첫 구현체가 3만개를 기준으로 했기 때문이다.
  • 부호 있는 자료형, 오버플로우 문제
    각 셀에 들어가는 숫자 자료형이 C언어로 치면 unsigned인지, signed인지도 구현체마다 제법 다른 편이다.
    특히 오버플로우 같은 경우, 어느 자료형이냐에 따라 값이 급격하게 바뀔 수 있기 때문에 일부러 오버플로우를 일으켜서 숫자를 줄이는 등의 트릭이 구현체에 따라 통하지 않을 수도 있음을 의미한다.
    구현체에 따라 특정한 자료형이 없어 이러한 오버플로 현상이 아예 존재하지 않거나, 0에서 1을 빼면 그냥 예외가 나버리는 경우도 존재한다.
  • 메모리 방향 문제
    보통 브레인퍽의 메모리 주소는 0부터 시작해 (방향을 굳이 정한다면) 오른쪽으로 계속 증가하는 형식인데, 주소가 0일 때 <를 어떻게 처리할지도 구현체별로 꽤나 다르다.
    대부분의 경우 음의 메모리 주소는 구현하지 않으며, 예외를 일으키거나 (시계처럼 끝까지 돌아서) 마지막 메모리 주소로 이동하는 경우도 존재한다.
  • EOF 처리
    만약 , 명령을 수행하던 중 EOF를 받았다면 어떻게 처리할지 역시 구현체별로 다르다.
    이게 왜 중요하냐 하면, 임의 길이의 문자열을 입력받아 거꾸로 뒤집는(reverse) 프로그램 등을 구현할 때, 어디까지 읽어야 하는지를 결정할 수 있어야 하는데, 보통 유닉스 스타일로는 어느 임의의 음의 정수 도 EOF가 될 수 있지만, 보통 -1을 사용한다.
    그런데 우어반 뮐러의 첫 구현체는 이런 거 없이 그냥 EOF가 반환되면 현재 포인터가 가리키고 있는 셀의 값을 변경하지 않고 그대로 두었다.
    이후 많은 구현체들이 같은 방식을 채용했지만, 상당히 많은 구현체들이 이런 방식을 사용하지 않으며, 주로 0을 반환한다. 앞서 말한대로 -1을 반환하는 경우도 드물게 존재한다.
    EOF는 (구현을 대충 했다는 가정하에) 특히 구현 언어에 영향을 많이 받는 편인데, C로 구현한 경우 -1을 준다던가 Python으로 구현된 경우 내장 input() 함수를 사용했다면 EOF를 마주치면 EOFError 예외가 발생하는 편이다.

상황이 이렇기 때문에, 굉장히 길고 복잡한 코드라면 브레인퍽 구현체별로 다른 결과가 나오거나 에러가 발생하는 일이 많은 편이다.

7. 기타

  • SeeU의 반도의 흔한 이별노래는 가사 전체가 하나의 브레인퍽 코드로 되어 있다.
  • 여기서 브레인퍽 프로그램을 실행할 수 있다. 여기서는 #가 중단점으로 사용된다.
  • 다른 양덕이 Geometry Dash로 구현한 브레인퍽 실행기도 있다. Geometry Dash의 근본적인 한계로 인해 프로그램을 저장할수도 없고 6분 정도의 시간제한이 있지만 그래도 대단한 수준이다.
  • 성인용품[5]이 이 프로그래밍 언어를 떠올리게 한다는 이 있다.


[1] 어떤 프로그래밍 언어나 추상 기계가 튜링 기계와 동일한 계산 능력을 가진다는 뜻이다. 튜링 기계는 계산 가능한 모든 함수를 계산할 수 있는 추상적 모델을 말하는데, 쉽게 말해서 이상적인 컴퓨터를 생각하면 된다. 한 가지 태클이 걸릴 점은, 현실 역사에선 튜링 기계란 개념이 나온 뒤에 그 개념에 기초해서 컴퓨터가 나왔다는 것.[2] The language's name is a reference to the slang term brainfuck, which refers to things so complicated or unusual that they exceed the limits of one's understanding, as it was not meant or made for designing actual software but to challenge the boundaries of computer programming. - 위키피디아[3] 왜냐하면 fuck의 의미가 심한 욕설로 기능하지 않고 단순히 '성관계하다'라는 뜻을 비속하게 나타낼 때도 있기 때문이다. 물론 이것도 욕이다 이런경우에서의 fuck은 '따먹다'가 적절한 번역.[4] 이 덕분에 별도의 주석 명령어 없이 그냥 평문을 써도 주석이 되는 셈이다.[5] TMA 산하의 브랜드가 만들었다.