본문 바로가기
오픈소스

[RustPython] Triple quote multiline strings not supported in interactive shell 문제 분석 및 해결

by run() 2022. 7. 11.

일러두기

RustPython은 Rust로 구현된 Python 인터프리터로, CPython과의 호환을 목표로 한다. 따라서 CPhython과 다르게 동작하는 부분은 모두 버그이며 CPython과 동일하게 동작하도록 수정해야한다.

이슈

https://github.com/RustPython/RustPython/issues/3760

 

현상

리스트 1.1 Cpython에서의 multiline string 동작

 

리스트 1.2 RustPython에서의 multiline string 동작

 

이슈 링크를 보면 괄호, 대괄호, 중괄호를 multiline으로 사용하려는 상황에서도 동일하게 문제가 발생한다.

 

원인 분석

1. 다른 Multiline 기능은 잘 동작하는가?

리스트 1.3 RustPython에서의 for문 동작

 

 

리스트 1.4 RustPython에서의 클래스 문법 동작

 

for문, 클래스 정의와 같은 또다른 multiline 기능은 RustPython에서 잘 동작한다. 따라서 입력을 제대로 받지 못하는 문제는 아니다. 이를 통해 """이나 [, (, { 등의 특정 상황에서만 제대로 해석하지 못하는 것으로 추측할 수 있다.

 

2. 내부 동작은 어떻게 다른가?

목표 지점 파악

RustPython은 진입점에서 모드를 구분한다. (run_rustpython 525라인부터 참고). Repl 모드에서는 shell::run_shell에 진입하여 사용자로부터 코드를 입력받고, 실행한다는 것을 알 수 있다.

 

run_shell 함수에서는 다음과 같은 단계로 실행된다.

 

1. repl.readline(prompt)로 사용자로부터 입력을 받음

이때, prompt에 담긴 문자열이 사용자에게 보여진다. 예를 들어, let prompt = ">>>>>"라면 위 코드 예시와 같이 입력창에 표시되는 구조이다.

 

2. 정상적인 입력이라면

repl.readline 의 리턴값은 ReadlineResult::Line(line) 가 되고, 이때 line에 입력한 내용이 담겨있다. 이 외에 다양한 상황이 발생할 수 있는데 이는 우리의 현재 목적에서 고려할 상황은 아닌 것으로 보인다.

 

3. 입력받은 코드를 실행

사용자의 입력을 검증하고 정상적인 입력으로 판단되면 shell_exec에 진입하여 해당 코드를 실행한다. 사실 shell_exec 함수가 하는 일은 compile 함수를 실행했을 때의 결과를 바탕으로 ShellExecResult를 만들어낸다.

 

리스트 1.5 run_shell에서 shell_exec의 결과로 콘솔을 다르게 보이게 하는 로직

 

ShellExecResult는 run_shell 함수에서 쓰인다. 결과에 따라 사용자 입력을 clear하고 >>>>>를 콘솔에 다시 띄우거나, ..... 을 띄워주면서 계속 입력받을 수 있게 한다. 그 외에 에러가 발생한 경우도 입력을 clear하는 동작을 한다.

따라서, 정상적인 상황에서는 ShellExecResult::Continue가 나와야 하지만, 지금은 ShellExecResult::PyErr이 발생하고 있을 것이라 예상할 수 있다.

 

실제로 어떻게 다른가?

리스트 1.6 shell_exec 함수 정의

 

a = """1 을 실행했을 때

shell_exec은 내부에서 vm.compile을 호출하는데, 디버거로 따라가다보면

CompileError { error: CompileErrorType::Parse(ParseErrorType::Lexical(LexicalErrorType::StringError)) } 가 발생함을 알 수 있다. 따라서 리스트 1.6에서 가장 아랫줄에 매칭되어 ShellExecResult::PyErr를 리턴한다. 콘솔에는 Syntax 에러 관련 메시지가 나오면서 콘솔이 비위지고 새로운 프롬프트가 뜬다.

 

for i in range(5): 를 실행했을 때

동일하게 디버거를 따라가보면

CompileError { error: CompileErrorType::Parse(ParseErrorType::Eof), .. } 가 발생한다. 따라서 리스트 1.6에서 두번째 패턴에 매칭되어 ShellExecResult::Continue를 리턴한다. 리스트 1.5에서 알 수 있듯이 이로 인해 계속 입력받을 수 있는 상황으로 이어진다.

 

해결 방법 계획

a = """1 과 같은 구문이나 a = [ 같은 미완성된 구문이라도 LexicalError를 발생시키지 말고, Eof 에러를 발생시켜야 계속 입력받을 수 있게 된다. 이를 위해서는 파서에서 관련 부분을 수정해야할 것으로 보인다.

 

1. ShellExecResult::PyErr가 아니라 ShellExecResult::Continue가 나오게 한다.
2. 이를 위해서는 오류를 발생시키는 문법을 파싱할 때 LexicalError가 아니라 EofError를 발생시켜야 한다.
3. LexicalError 대신 EofError를 발생시키려면 Lalrpop에서 파이썬 문법 파싱하는 부분을 수정해야 할 것 같다. (추측)

 

해결

모든 parser, lexer 구조를 파악하는 것은 현실적으로 불가능하므로 에러 메시지 위주로 코드에서 포인트를 확인해보기로 했다. 현재 문제는 """이나, 괄호를 이용한 multiline 문법에서 에러가 발생한 상황이다. 그래서 관련 키워드 위주로 코드베이스에서 검색을 했는데 운이 좋게도 triple_quote 키워드를 lexer.rs에서 발견할 수 있었다.

 

관련성이 높은 코드를 발견하면 해당 지점부터는 파악하기 어렵지 않다. 코드를 살펴보면 lexer가 동작하는 로직을 어느정도 파악할 수 있다. 이때부터는 디버거를 이용해서 오동작하는 경우에 어떤 분기를 타는지 분석하여 해결했다.

 

https://github.com/RustPython/RustPython/pull/3874

 

Fix multiline nesting bug by kth496 · Pull Request #3874 · RustPython/RustPython

Fix #3760

github.com

 

https://github.com/RustPython/RustPython/pull/3885

 

Fix multiline string bug by kth496 · Pull Request #3885 · RustPython/RustPython

Fix #3760 Related PR: #3874

github.com

 

하나의 PR에 작업을 하려 했는데 중간에 머지되어서 2개로 나뉘었다.

 

괄호를 이용해서 여러줄에 표현식을 작성할 때 발생하는 NestringError와 """를 이용해 multiline string을 작성할 때 발생하는 StringError를 해결했다. 이 과정에서 expectedFailure로 처리되어있던 CPython unit test 케이스 하나도 함께 해결했다.

 

여담

RustPython에서 스크립트 파일을 작성해 실행할때는 아무런 문제가 발생하지 않았는데, 왜 REPL 모드에서만 이런 문제가 생기는지 궁금했었다. 살펴보니 스크립트를 읽어올때는 전체 스크립트 파일을 하나의 문자열로 읽어온다. 반면, REPL 모드는 한 줄마다 엔터를 입력하면 즉시 컴파일해서 의미를 해석한다. 따라서 다음 문장으로 이어질지 아닐지를 매 순간 판단해야 한다. 기존에는 이러한 유즈케이스가 아예 고려되지 않았고, 스크립트 파일 실행에는 문제가 없었기 때문에 크게 인식하지 못했던 것으로 보인다.

댓글