Cotton
마음에 드는 것만
골라 만드는 언어.
Rust의 안전성과 Python, JavaScript의 편한 문법을 섞은 식(expression) 중심 언어입니다. 언어 설계를 쓰고, 설계한 문법 전체를 실행해 볼 수 있는 레퍼런스 인터프리터를 C++17로 만들었습니다.
요약
- 4,134줄
- C++17로 쓴 렉서, 파서, 인터프리터
- 0개
- 외부 라이브러리. g++만 있으면 빌드됨
- 5개
- 저장소에 들어 있는 예제 프로그램
- 69%
- 파서와 인터프리터가 차지하는 비율. 2,862줄
설계 방향
핵심 생각은 어려운 일은 컴파일러가 떠안고, 사람은 짧고 읽기 쉬운 코드를 쓴다입니다. 소유권 추적, 타입 추론, 범위 검사, 최적화는 컴파일러가 맡습니다. 그러면서도 실행 중에 숨겨진 일은 두지 않습니다. GC 멈춤도, 몰래 풀리는 예외도, 요청하지 않은 동적 디스패치도 없습니다.
세미콜론, 조건식을 감싸는 필수 괄호, null, 데코레이터가 없습니다. 기본은 불변이고 수정자는 mut 하나, 공개 여부는 pub 하나뿐입니다.
어디서 가져왔나
| 언어 | 가져온 것 |
|---|---|
| Rust | 소유권과 빌림, enum과 패턴 매칭, trait, Result/Option, 식 블록, 비용 없는 제네릭 |
| JavaScript | 템플릿 문자열, 구조 분해, spread/rest, async/await, |x| x*x 형태의 클로저 |
| Python | 컴프리헨션, for x in y, in·not·and·or 키워드, 간단한 import |
문법 맛보기
패턴 매칭
match value {
0 => print("zero"),
1 | 2 => print("one or two"),
3..=9 => print("small"),
n if n < 0 => print("negative"),
_ => print("other"),
}에러 처리: 예외도 null도 없음
fn divide(a: f64, b: f64) -> Result<f64, str> {
if b == 0 { return Err("division by zero") }
Ok(a / b)
}
let value = divide(10, 2)? // 에러면 바로 위로 전달
let safe = divide(10, 0) ?? 0.0 // 에러면 기본값
let len = name?.len() // 옵셔널 체이닝?는 에러를 호출한 쪽으로 넘기고, ??는 기본값을 주고, ?.는 값이 없으면 멈춥니다.
실제로 돌려 보기
저장소의 examples/comprehension.cot를 레퍼런스 인터프리터로 실행한 결과입니다.
fn main() {
let nums = [1, 2, 3, 4, 5]
let squares = [n * n for n in nums]
print(`Squares: ${squares}`)
let evens = [n for n in nums if n % 2 == 0]
print(`Evens: ${evens}`)
let pairs = [(i, j) for i in 0..3 for j in 0..3 if i != j]
print(`Pairs where i != j: ${pairs}`)
let lookup = {x: x * x for x in nums}
print(`Lookup squares dict: ${lookup}`)
let [first, ...rest] = nums
print(`First: ${first}, Rest: ${rest}`)
let (a, b, c) = (1, 2, 3)
print(`Destructured: ${a}, ${b}, ${c}`)
let spaced = [1, 2, ...[3, 4], 5]
print(`Spread: ${spaced}`)
}$ ./cotton examples/comprehension.cot
Squares: [1, 4, 9, 16, 25]
Evens: [2, 4]
Pairs where i != j: [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
Lookup squares dict: {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}
First: 1, Rest: [2, 3, 4, 5]
Destructured: 1, 2, 3
Spread: [1, 2, 3, 4, 5]
구조
렉서가 토큰을 만들고, 파서가 재귀 하강과 Pratt 파싱을 섞어 AST를 만들고, 인터프리터가 AST를 직접 따라가며 실행합니다. 지금은 인터프리터지만 문법 전체를 먼저 돌려 보기 위한 레퍼런스 구현이고, 이후 바이트코드 컴파일러와 LLVM 백엔드로 넘어갈 계획입니다.
구현하며 정한 것
세미콜론 없이 줄 끝 처리
렉서가 소괄호, 대괄호, 중괄호 깊이를 따로 셉니다. 깊이가 0일 때만 줄바꿈을 문장 끝으로 내보내서, 괄호 안에서는 여러 줄로 써도 문장이 끊기지 않습니다.
구조체 리터럴과 블록 구분
IDENT { 뒤가 필드: 값 형태이거나 비어 있을 때만 구조체 리터럴로 봅니다. 그렇지 않으면 식별자 다음에 블록이 온 것으로 봅니다. if cond { ... }를 구조체로 잘못 읽는 Rust 계열 문법의 고전적인 모호함을 이렇게 풀었습니다.
소유권
환경에 변수마다 값, 가변 여부, 이동 여부를 저장합니다. 복사 타입(정수, 불, 실수, 참조)이 아닌 값은 함수에 넘기면 이동된 것으로 표시되고, 이동된 값을 쓰면 실행 중 에러가 납니다. 정식 컴파일러에서는 이 검사를 컴파일 시간에 할 계획입니다.
제어 흐름
break, continue, return, ?의 에러 전달을 내부 신호로 만들어 호출 스택을 깔끔하게 되감습니다.
메서드 호출
impl 블록의 메서드를 타입 이름과 메서드 이름으로 등록해 두고, obj.method(args)를 만나면 찾아서 self를 앞에 붙여 호출합니다.
구현 현황
- let / mut / const, 암묵적 반환완료
- 함수,
|x|클로저완료 - if, for in, while, loop, 레이블 break/continue완료
- 패턴 매칭 (OR, 범위, 가드, 구조체·enum 패턴)완료
- struct, enum, impl, trait완료
- Result/Option,
?,??,?.완료 - 배열·딕셔너리·튜플, 컴프리헨션, 구조 분해, spread완료
- 템플릿 문자열, 여러 줄 문자열완료
- Box, &, &mut, unsafe완료
- 제네릭 (파싱까지, 단형화는 예정)일부
- spawn, async/await, channel (파싱까지, 동기 실행)일부
- import, module, pub (파싱까지)일부
- 소유권 검사 (실행 중 검사, 컴파일 시간 검사는 예정)일부
- 바이트코드 컴파일러, LLVM 백엔드계획
- 수명 추론을 포함한 컴파일 시간 빌림 검사계획
- Hindley-Milner 타입 추론계획
- work-stealing 비동기 런타임계획
도구
VS Code 확장(cotton-vscode 0.1.0)도 만들었습니다. .cot, .cotton 파일을 인식하고, 키워드와 타입, 템플릿 문자열 안의 ${식}까지 구분해 색을 입히며, 괄호 짝 맞추기와 fn, match, impl 같은 스니펫을 제공합니다.
기록
- 저장소 생성, 개발 환경 구성, 토큰 정의와 렉서 구현
- 파서, 값 표현 추가, dev 브랜치 병합