## Python으로 DSL 만들기: 졸업 프로젝트를 위한 러시아어 인터프리터 제작기
LawScript 인터프리터는 단순한 학술 실험이 아닙니다—법적 과제를 해결하기 위해 Python으로 만든 실용적인 DSL로, 명령형 하위 시스템을 갖추고 있습니다. 저자는 선언적 계약 설명과 규칙 검증을 위한 절차적 로직을 결합해 학과에서 졸업 논문 프로젝트로 승인받은 하이브리드 언어를 만들었습니다. 핵심 요소는 별도의 렉서 없이 동작하는 파서, 라인 메타데이터를 가진 전처리기, 그리고 "바이트코드"로 직렬화된 AST로의 컴파일입니다.
렉서 없는 아키텍처: 바퀴를 새로 발명하는 게 장점인 경우
전형적인 "렉서 → 파서 → 컴파일러 → 인터프리터" 파이프라인을 일부러 깨뜨린 구조입니다: 어휘 분석은 파서에 직접 내장되어 있습니다. 소스 코드의 각 줄은 라인 번호, 파일 경로, 원본 텍스트를 저장하는 Line 클래스로 감싸집니다. 이렇게 하면 오류 맥락을 보존하고 디버깅이 간단해집니다. 예시 구조:
class Info(NamedTuple):
num: int
file: str
raw_line: str
class Line(str):
def __new__(cls, value: str, num: int = 0, file: str = ""):
obj = str.__new__(cls, value)
obj.raw_data = value
obj.num = num
obj.file = file
return obj
def get_file_info(self) -> Info:
return Info(num=self.num, file=self.file, raw_line=self.raw_data)
전처리기는 VKLYuChIT 지시어를 처리하며, 현재 디렉터리나 표준 라이브러리에서 모듈을 재귀적으로 불러옵니다. .raw (소스 파일), .law (직렬화된 AST), .pyl (Python 확장) 세 가지 파일 형식을 지원합니다. 가져오기는 재처리를 피하기 위해 캐싱됩니다.
컴포지션을 통한 파싱: 문법을 마이크로서비스 집합으로
파서는 추상 베이스 클래스로 구현되었으며, 각 문법 구성 요소에 특화된 파서가 이를 상속합니다. 이를 통해 파서를 중첩할 수 있습니다: 익숙한 구성 요소를 만나면 현재 파서가 자식 파서에게 위임합니다. 결과는 MetaObject 트리로, 이를 검증하고 실행 가능한 객체로 변환합니다.
어휘 분석은 separate_line_to_token 메서드가 담당하며, 줄을 토큰으로 분리하면서 주석을 무시하고 괄호 균형을 확인합니다. 오류 발생 시 위치 힌트를 제공합니다—커서가 문제 있는 기호를 가리킵니다. 예를 들어 표현식에 불필요한 쉼표가 있을 때:
if token_ == Tokens.right_bracket:
sub_expr = expr[offset:]
previous_tok = sub_expr[offset_ - 1]
if previous_tok == Tokens.comma:
err_expr = ''.join([str(i) for i in sub_expr][:offset_+1])
sub_expr = [str(i) for i in sub_expr]
res_expr = ''.join(str(i) for i in expr)
target_comma = (
f"{err_expr}\n"
f"{' ' * (sum(len(t) for o, t in enumerate(sub_expr) if o < offset_ - 1))}^"
)
raise InvalidExpression(
f"In vyrazhenii: '{res_expr}' worth lishnyaya zapyataya '{Tokens.comma}'\n\n"
f"{target_comma}\n"
)
역폴란드 표기법과 복잡한 표현식
기본 인수를 포함한 모든 복잡도의 표현식은 RPN 스택으로 컴파일됩니다. 이렇게 하면 연산자 우선순위와 중첩 함수 호출을 정확히 처리합니다. 예를 들어 배열 정렬 프로시저:
OPREDELIT PROTsEDURU sortirovka_array(array_numbers) (
SET length = length_array(array_numbers);
SET minimum_index = 0;
LOOP index FROM 0 TO length-1 (
minimum_index = index;
LOOP internal_index FROM index+1 TO length-1 (
IF fetch_from_array(array_numbers, internal_index) MENShE fetch_from_array(array_numbers, minimum_index) THEN (
minimum_index = internal_index;
)
)
IF minimum_index NERAVNO index THEN (
SET temporary_variable = fetch_from_array(array_numbers, index);
change_in_array(array_numbers, index, fetch_from_array(array_numbers, minimum_index));
change_in_array(array_numbers, minimum_index, temporary_variable);
)
)
NAPEChATAT array_numbers;
)
이것은 다음과 같은 AST로 변환됩니다:
[
["AssignField", "TARGET", "length", "EXPR", [...]],
["AssignField", "TARGET", "minimum_index", "EXPR", [Number(0)]],
["Loop",
"FROM_EXPR", [Number(0)],
"TO_EXPR", [Service name: <length>, Number(1), Service name: <->],
[
["AssignOverrideVariable", "TARGET_EXPR", [...], "OVERRIDE_EXPR", [...]],
["Loop",
"FROM_EXPR", [...],
"TO_EXPR", [...],
[
["When", "EXPR", [...],
[["AssignOverrideVariable", ...]]
]
]
],
["When", "EXPR", [...]],
[["AssignField", ...], [...], [...]]
]
],
["Print", "EXPR", [Service name: <array_numbers>]]
]
컴파일러와 실행: 트리에서 액션으로
컴파일러는 AST를 순회하며 각 노드를 실행 가능한 객체로 변환합니다. 예를 들어 AssignField는 변수 할당이 되고, Loop는 런타임에 계산된 경계의 루프가 됩니다. 인터프리터는 이러한 객체를 순차적으로 실행하며 호출 스택과 로컬 변수를 관리합니다. 재실행 속도를 높이기 위해 AST를 .law 파일—"바이트코드"—로 직렬화해 재파싱 없이 불러옵니다.
핵심 포인트
- 이중 성격의 DSL: 선언적 계약 + 명령형 로직 = 법적 작업에 특화된 독특한 하이브리드.
- 라인별 메타데이터:
Line클래스가 정확한 오류 진단을 위한 맥락을 보존합니다. - 파서 컴포지션: 각 문법 구성 요소가 자체 클래스로, 언어 확장이 쉽습니다.
- 표현식용 RPN: 외부 의존성 없이 복잡한 산술과 기본 인수를 처리합니다.
- AST 직렬화:
.law파일이 캐시 역할을 해 초기 컴파일 후 실행 속도를 높입니다.
— Editorial Team
아직 댓글이 없습니다.