기존에 만들던 토이플젝 스택기반언어를 개선하고 있다.


변태적인 언어 구상하는 사람 보고나서

나도 내 조잡한 언어의 변태같은 기능에 대해서 한번 정리하는 글을 써보려고 한다.


전처리기 > 컴파일러 > 바이트코드 인터프리터의 3단계로 작동한다.

컴파일러라 해봐야 함수나 제어문, 반복문을 구조적으로 해석해서 OP코드로 변환시켜주는 것 뿐인 간단한 방식이다.

제어 블럭이 존재하고 다양한 토큰이 나뉘어진 brainfuck이라고 생각하면 된다.


기존 구현체에 대한 마지막 글

만들고 있는 언어 매크로 개선저번까지 만들었던 건#을 이용해 컴파일러에 문자열 토큰을 따로 넘기는 것. 그걸로 매크로를 만들었던 것.이번에는 그 넘겨받은 토큰들을 따로 컴파일하거나 합치는 기능을 추가해서 일종의 메타프로그래밍 기능으로 확장시켰음.gall.dcinside.com


부끄러운 스파게티 코드 기존 구현체 레포.

GitHub - sabrlang/sabr: Stack-oriented programming languageStack-oriented programming language. Contribute to sabrlang/sabr development by creating an account on GitHub.github.com



지금 새로 개선중인 구현체는 컴파일러만 만든 상태.

바이트코드 해석기는 기존 구현체랑 거의 비슷할 것 같다.


매크로에도 스택 기반 프로그래밍 패러다임을 적용하고 싶었다.


저때 매크로를 구현할 당시에는 전처리 방식이 아니었는데

이번에는 미리 코드를 전처리할 때 풀어쓰도록 변경했다.

또한 예전에는 #가 붙은 토큰을 매크로 해석용 스택에 따로 삽입해서 컴파일 시 해석했는데 이게 좀 거슬렸다.

통일감이 없다고 해야하나, #이 붙은 토큰만이 전처리기에 쓰일 것이라 생각하니까 기분이 나쁘다.


이번에는 어떤 토큰이든 스택기반으로 해석하게 하고 전처리기 명령어를 C언어 마냥 #을 붙이게 바꿔주었다.


간단한 예시를 보자.

$sayhello { "hello" puts } #macro sayhello

$a b #macroab로 치환한다.

중괄호로 감싸면 한 덩어리의 특별한 토큰으로 취급하는데, 이것을 매크로나 함수로 정의하면 호출 시 풀어지도록 구현했다.

sayhello"hello" puts로 치환될 것이다.


그런데 이런 단순치환 매크로 만으로는 코드를 조작하기가 힘들다.


나는 전처러기 역시 스택 기반 프로그래밍이 되어야한다고 생각했다.

토큰을 문자열로 해석하기도 하고, 값으로 해석하기도 하고, 코드로 해석할 수도 있는 병신같은 시스템을 만들었다.

그리고 코드를 읽어나가면서 스택에 삽입했다가 연산할 수 있는 그런 방식으로 전처리기를 구성했다.


애초에 전처리기 말고 구현중인 언어 자체도 Forth처럼, 타입이 존재하지 않는 괴짜언어이다.

부동소수점, 부호 있는 정수형, 부호 없는 정수형, 식별자, 메모리 주소 모두 구분하지 않는다.


또한 전처리기에서 함수(재귀호출을 위한 지역범위 보유), 매크로(단순 치환)을 분리했다.


그래서 이런 짓거리도 가능하도록 구현했다.

$sayhello { $count #swap #lmacro puts count 1 #> { count 1 #- sayhello } {} #if } #func 3 sayhello


3 sayhello 가 코드상에 있으면

중괄호가 풀어지면서 해석하게 되는데

3 $count #swap #lmacro "hello" puts count 1 #> { count 1 #- sayhello } {} #if

그럼 이렇게 될 것이다.


#lmacro는 함수의 지역 범위 안에서만 매크로를 선언한다.

b $a의 식별자가 뒤에 있는 형태로 되어 있으므로 #swap을 통해 순서를 바꿔주고 매크로 선언한다.

sayhello 함수 범위 안에서 count 토큰을 발견하면 3으로 치환될 것이다.


count 1 #> 는 3이 1보다 큰지 비교하여 참/거짓에 따라 1 또는 0을 남긴다.

a b c #ifa가 참일 경우 b를 실행, 아니면 c를 실행한다.

31보다 크므로, count 1 #- sayhello 를 실행한다.

count 1 #-3에서 1을 뺀 값을 코드에 남긴다.

2 sayhello 가 실행된다.


3이나 2같은 숫자 토큰도 전처리기에서 문자열이지만

전처리기 연산 명령어가 등장하면 역할에 맞는 타입으로 가정하여 파싱하여 잠시 수로 변환했다가,

연산 결과를 다시 맞는 타입으로 토큰에 출력한다.


3.2 2.5 #f+ 라고 하면 앞의 두 토큰이 부동소수점일 것이라 가정하고 덧셈하는 것이다.

Forth에서도 타입이 없어서, (부동소수점은 별도의 스택을 사용하는 점도 있지만)

부호 있는 정수, 부호 없는 정수, 부동소수점용 연산자가 각각 존재한다. 나도 똑같은 방식으로 단어를 구성했다.

괴랄하게 보이겠지만 typeless 언어에서는 당연한 선택이 아닐까나...


아무튼간에 이런 식으로 반복적 재귀호출이 가능하다.

1 sayhello 까지 가다가 마지막엔 조건이 안 맞아서 빈 토큰을 실행하고 계속 반환되어 올라올 것이다.

결국 코드에는

"hello" puts "hello" puts "hello" puts

가 남는다.


뭐 이런 식으로 이것저것 넣으니까

전처리기에서 함수, 매크로, 사칙연산, 비교연산, 비트연산, 조건문, 반복문이 가능하게 만들었다.

거의 전처리기가 프로그래밍 언어인 셈 아닌가?


디버깅용으로 토큰의 내용을 출력하는 #echo 명령어도 있고,

애초에 토큰을 조작하기 위해 넣은 전처리기인만큼

기본적인 문자열 슬라이싱과 인덱싱도 가능하게 만들었다.


어차피 기계어로 바로 변환될 것도 아니고,

전처리기 만든 건 별로 효율이 좋아질 것 같지도 않은데 그냥 만들어보고 싶었다.



이런 Forth 스타일의 문법은 리스프처럼 지독하리만큼 간단해서,

여러 구조적인 어려움을 가지고 있지만 가지고 놀기엔 제일 쉬운듯 하다.