본문 바로가기

프로그래밍 기록/토이

주술회전 대사로 프로그래밍을 해보자

Lee-WonJun/Jujutsu-Kaisen-lang: 주술회전 명대사 프로그래밍 언어 (github.com)

GitHub - Lee-WonJun/Jujutsu-Kaisen-lang: 주술회전 명대사 프로그래밍 언어

주술회전 명대사 프로그래밍 언어. Contribute to Lee-WonJun/Jujutsu-Kaisen-lang development by creating an account on GitHub.

github.com

 
일전에 [LolChatLang] 롤챙 프로그래밍 언어를 만들어보자 (tistory.com) 에서 F# 으로 프로그래밍언어를 만들어 봤는데, 이때는 monadic parser 를 사용하지 않고 한땀 한땀 만들었다.
이번 설 연휴에 뭐라도 해볼까 하다가, monadic parser를 한번 써보기로 결정하고, 적당하고 재밋는 언어를 만들어볼까 하다가

아는 형이 갑자기 주술회전 드립을 치길래, 이거다 하고 만들어보았다.
주술회전은 시부야 사변까지 밖에 안보긴 했는데...
짤로 많이 돌아다는 드립들을 모으고 모아다가 적당한 문법을 정의하고, AST / Parser / Interpreter 작성하면 짜잔 언어 완성!
이번에는 manadic parser (로 간주할 수 있는)  fparsec 을 사용했는데,
대부분의 AST 와 Parser 사용법은 Parsing Programming Languages with FParsec 을 참고했다.
 

Fparsec 

확실히 코드가 확 줄고, 그냥 조합기 쓰까섞으면 알아서 판단하므로, 롤챙을 만들었을때에 비하여 훨신 편했다.

롤챙 수동 파서 작성할때
Fparsec 으로 슥슥 조합할때

 
모나드 문법을 쓰면 더 직관적인데 Fparsec 사용시에 monad expression 을 쓰지 않는게 권장되는것 같아서 안썻다. 속도문제가 큰거 같은데, 나의 경우 속도가 중요한건 아니라 큰 문제는 없지만, 이왕 하는김에 권장사항을 따르기도 했고, 또 이참에 raw 하게 좀 조합해 보고 싶었다. 

let ws = spaces // whitespace parser


// F# 의 Computation Expressions(즉 모나드 문법) 을 안쓰면 짧고 간결하다
let str_ws str = skipString str >>. ws

let number_ws = pfloat .>> ws

let pairOfNumbers = between (str_ws "(") (str_ws ")")
                            (tuple2 number_ws number_ws)
                            
// F# 의 Computation Expressions 을 쓰면 직관적이고 보기 좋다
let str_ws str = parse {do! skipString str
                        do! ws
                        return ()}

let number_ws = parse {let! number = pfloat
                       do! ws
                       return number}

let pairOfNumbers = parse {do! str_ws "("
                           let! number1 = number_ws
                           let! number2 = number_ws
                           do! str_ws ")"
                           return (number1, number2)}

 

 

언어는 어떻게 만드나?

0. 문법을 정의하자 

꼭 문법을 먼저 정의할 필요는 없긴할것 같은데, 일단 나는 주제가 주제인 만큼 문법을 먼저 정의했다.
인터넷에 주술회전 밈, 주술회전 웃음벨등을 검색.. 적당한 대사들을 가져왔다.
적당한 대사를 적당한 기능에 엮을수 있을지 생각해보고 괜찮은것 같으면 채용!

네놈은 {변수명} 마저 {값 | 변수명 }이란 말이냐

예를 들어 위에 내용으로 변수를 초기화 해야겠다고 결정했다.
그리고 편의상 정수만 계산하기로 결정
BNF, LL 파서, LR 파서, 재귀 하향, 왼쪽 재귀, 엡실, 터미널 등등.. 은 잘 모르겠고..  한계상 구현하기 어렵거나 구현불가능한 문법이면 쿨하게 버리거나 문법 수정

1. AST 를 정의하자

사실 문법은 컴퓨터 입장에서는 별볼일 없는 데이터이다. 거품을 쫙빼고 컴퓨터가 받아드리기 쉬운 형태인 Tree 데이터로 데이터를 잡자
즉 거품뺀 구문에 대한 트리라 abstract syntax tree (AST) 이다.

네놈은 -> 필요없음
{변수명} 
마저 -> 필요없음
{값 | 변수명}
이란 말이냐 -> 필요없음

변수를 초기화 하는 구문에서 필요한건 초기화 할 "변수명" 과  들어갈 값인 "정수형" or 또다른 "변수명" 이다
이걸 고대로~~ 코드로 옮기면 된다.

type LiteralInt = int // 정수형
type VariableString = string // 변수명
type Value = // 정수형 or 변수명
    | Literal of LiteralInt
    | Variable of VariableString

type Statement =
    | Set of name:VariableString * value:Value // 초기화 (Set) 는 선언한 변수명과 들어갈 값인 (정수형 or 변수명) 이 필요

ML 류 언어 ADT 정의는 세계 제일!

2. AST 를 만들자

이제 string 을 읽어서 적절히~ 위의 AST 로 바꾸면된다. 즉 Parser 를 짜는것이다.
수동으로 뚝딱뚝딱 string 직접 읽어가면서, 자르고 붙여서 만들어도 되지만, 수많은 석박이 만든 툴을 사용하자. Fparser 되시겠다.

// 변수나 값 자체를 위한 parser 들 생성 feat GPT ...
let variableParser =  many1Satisfy2L isLetter (fun c -> isLetter c || isDigit c) "변수명" .>> spaces |>> VariableString
let literalParser =  pint32 .>> spaces |>> Literal
let valueParser = choice [literalParser; variableParser |>> Variable]

pstring "네놈은"
 >>. spaces
 >>. variableParser 
 .>> pstring "마저" 
 .>> spaces 
 .>>. valueParser  
 .>>  spaces 
 .>> skipString "이란 말이냐"

네놈은 으로 시작해야한다 (pstring) / 띄어쓰기 (spaces)
변수명이 필요하다 (variableParser) 
마저 가 들어가야 한다 (pstring) / 띄어쓰기 (spaces)
이 필요하다 (valueParser) / 띄어쓰기
이란 말이냐 는 있어도 그만 없어도 그만 (skipString)
이렇게 문법적으로 필요한 애들만 조합기를 조합해서쓰면된다.
여기서 fparser 의 경우 >>. .>> .>>. 을 사용하는데, 대충 저 점과 점 사이가 필요한 데이터라고 보면된다..
 

3. AST 를 해석하자

이제 저 AST 를 실제로 해석하는 친구(Interpreter) 만 작성하면 인터프리터 완성~ 대충 바로 돌리면 인터프리터고 이게 바이트코드를 생성하게 하면 컴파일러다

 match statement with
    | Set (name, value) -> setValue ctx name value
    | // 나머지 ..

 

Pros Cons

장점은
1. 조합하기 편하다. 적당히 choice / skip / applicative 섞어서 적기만 하면 대충 다 된다. 롤챙작성할때는, 다음에 나오는 구문에 따라서 처리하기 어려워서 Line 한개를 통채로 가져와서 분석했었는데, 모나딕 파서는 그럴 걱정이 없다.
2. 이렇게 조합하기 편한 조합기가 많다. 그냥 가져다 쓰면된다
3. 알아서 지원되는 재귀적 함수. 가변 변수 클래스로 만들어져있는 클래스가 있어서 쓰고 설정만 해주면됨
4. 더 안전한 코드. 어느부분에 에러나는지도 알려준다.
단점을 보자면
1. 처음에 이해하기 쉽지않음 처음에는 >> .>> >>. .>>. 들이 섞여있어서 코드보고 이해하기 도 어려웠다.. (. 위치에 따라서 문맥이 바뀜)
2. 이렇게 조합하기 편한 조합기가 너무 많다, GPT 없으면 진작에 때려침 ㄷㄷ
3. 공부하면서 학습한 단점 : 하향식 파서의 단점을 그대로 가진다. (왼쪽 재귀 처리 불가능등)
 

결론

2초 휴재애애앳!!!
네놈은 최강 마저 5 이란 말이냐..
네놈은 사랑 마저 최강 이란 말이냐~~
네놈은 저주하는말 마저 최강 이란 말이냐!!
네놈은 고죠사토루 마저 최강 이란 말이냐!!
하! 마지막에는 저주하는말 을 내뱉어야지
넌 고죠사토루 여서
    게속해서 가르쳐 주겠어 사랑 을! 
        네놈은 사랑 마저 사랑 이란 말이냐~
        하! 마지막에는 사랑 을 내뱉어야지
    훗 에? 훗
인건가
아니면 고죠사토루 이라서
	하! 마지막에는 저주하는말 을 내뱉어야지
인건가
이건 고죠사토루 의 승리야
이건 최강 의 승리야
작별이다 최강 내가 없는 시대에 태어났을 뿐인 범부여

이게 언어냐

1. 작은 힘에는 작은 책임이 따르므로, 이런 toy 언어를 만드는건 재밋다
2. 예전에 모나드 볼때 >>= >=> >> <|> 이런거.. 누가 이렇게 쓰냐! 생각했는데 쓸땐진짜 편하다 진짜
3. Parser가 대표적인 monad 중에 하나인데 왜  FP 타입 순수 주의자들이 모든걸 모나드로 해결하려하는지 알것같긴하다.


728x90