- C3는 C와 함께 쓰는 시스템 언어를 목표로 하며, 모듈 시스템, 제네릭, 컴파일 타임 실행, 오류 처리,
defer, 슬라이스,foreach같은 기능으로 C 개발자의 전환을 쉽게 만듦 - 문법은 C에 가깝지만
fn,foreach,defer, implicitbreak,nextcase, 타입 메서드, 계약(contracts)처럼 표현력과 안전성을 보강하는 장치가 많음 - Gentoo에서 소스 빌드를 시도할 때 LLD 라이브러리 탐지 문제가 있었고, 결국 사전 빌드된
c3c 0.7.1로c3c init,c3c run흐름을 확인함 - 계산기 구현 과정에서
tmem임시 할당자,@pool, 표준 라이브러리List, optional 오류 처리,!재전파,nextcase가 유용했지만 언어 서버 지원 부족과 양끝 포함 슬라이싱은 불편했음 - C3는 C보다 안전하고 C++, Rust보다 단순하며 빠르게 개발할 수 있는 대안으로 보이지만, 매일 쓸 언어로는 Zig를 더 선호하고 C 대체 후보로 계속 써볼 만함
C3가 노리는 위치
- C3는 C를 기반으로 삼으면서 C와 함께 작동하는 언어를 목표로 하며, 표준 C에서 표현하기 어렵거나 불가능한 기능을 제공함
- 주요 기능은 모듈 시스템, 연산자 오버로딩, 제네릭, 컴파일 타임 실행, 의미론적 매크로, 통합 빌드 시스템, 오류 처리,
defer, 값 메서드, enum 연관 데이터, distinct type과 subtype, 점진적 계약, 내장 슬라이스,foreach, 동적 호출과 타입 등임 - 학습 과정은 실시간 기록에 가까워 장점과 불편함이 바로 드러나지만, 언어 이해가 다른 자료보다 부정확할 수 있다는 전제가 있음
C와 닮았지만 더 많은 기본기
- Hello World 예제는
import std::io;,fn void main(),io::printn("Hello, World!");형태라 C와 비슷한 첫인상을 줌 - C3의 import는 하위 모듈을 재귀적으로 가져올 수 있고, 이름 충돌이 생기면
abc::Context처럼 더 완전한 이름으로 해결함 - 표준 출력 함수는 전달된 대부분의 타입을 출력할 수 있어 print 디버깅이 C보다 편해 보임
printf는%s,%d,%f같은 C 스타일 포맷을 유지함%s는 enum 값REALLY_WARM도 문자열처럼 출력할 수 있음
제어 흐름: foreach, switch, nextcase
foreach는 슬라이스 값을 순회하며, 변수명 앞에&를 붙이면 참조 기반 반복이 가능함break와continue는 예상한 방식으로 동작함- 일반
for보다 의도를 잘 표현해 로직 오류를 줄일 수 있지만, C스럽지 않다는 인상도 남김
while조건 안에서 변수를 선언할 수 있어 C의 일반적인while보다 확장된 형태를 가짐switch는 C와 닮았지만 implicit break를 제공함- 빈
case는 허용되지 않아 명시적break가 필요함 - enum 전체 값을 이미 처리한 뒤
default를 두면 컴파일러가 경고함 - 각
case는 자체 스코프를 시작함
- 빈
nextcase는 다음 case로 fallthrough하는 키워드이며, 런타임 case를 지정해 jump table처럼 사용할 수 있음- Duff's device 예제를 C3에서 깔끔하게 작성할 수 있음
@jump속성은 최적화 컴파일러가 switch를 jump table로 만들도록 강제함
defer와 리소스 정리
defer는 스코프 종료 시 역순으로 실행되며, 주로 리소스 정리에 쓰임- 리소스를 획득한 직후
defer를 배치하면 모든 종료 경로에서 정리를 기억해야 하는 부담이 줄어듦 - C에서 흔히 쓰는 함수 끝의
gotocleanup 블록보다 훨씬 낫다고 평가함 errdefer와okdefer에 해당하는 동작은 각각defer catch,defer try로 표현할 수 있음
구조체, 메서드, 타입 정보
struct는 필드, 함수 포인터, enum, named sub-struct, anonymous sub-struct, anonymous union, named sub-union을 포함할 수 있음- 구조체 내부의 anonymous
union은 enum과 함께 tagged union을 구현하는 깔끔한 방식으로 보임 - C에서도 비슷한 구성이 가능하지만, C3 예제는 구조체 안에서 데이터를 저장하고 접근하는 방식이 더 명확함
- C3는 구조체, union, enum뿐 아니라 primitive를 포함한 모든 타입에 메서드 형태의 dot syntax 함수를 붙일 수 있음
- 모든 타입에는
.alignof,.kindof,.nameof,.sizeof,.typeid,.methodsof,.is_eq,.is_ordered같은 컴파일 타임 프로퍼티가 있음- 이 프로퍼티들은 메타프로그래밍에 유용함
- Zig에서 비슷한 정보를 얻으려면
@typeInfo를 사용해야 함
오류 처리와 optional
- C3의 optional 타입은 타입 뒤에
?를 붙여 표현함int? a = 1은 값이 있는 optional임- optional이 비어 있으면 원인을 설명하는 Excuse를 가짐
faultdef로OOPS,USER_ERROR같은 fault를 정의할 수 있음
catch로 optional이 비어 있는지 검사하고,!연산자로 오류를 재전파함- optional을 검사한 뒤 해당 스코프를 빠져나가면 남은 스코프에서는 자동으로 unwrap됨
- optional이 식에 포함되면 결과도 optional이 됨
- C3는 Zig의
error{SomeError}!i32와?i32, Rust의Result<i32, SomeError>와Option<i32>역할을 하나의 시스템으로 합친 것처럼 보임 - 이 설계는 “반드시 값이 기대되지만 실패할 수 있는 함수”와 “값이 없을 수 있는 함수”의 구분을 흐리게 함
- 대신 반환값을 처리할 때 두 타입 체계를 구분할 필요가 없다는 장점도 있음
계약과 매크로
- C3는
<*와*>안에 precondition/postcondition 계약을 둘 수 있음@require foo != null@ensure return > foo.x
- 계약은 파싱되는 주석 형태이며, 단순 런타임 assertion을 넘어 호출 체인으로 조건을 전파하고 컴파일 타임 folding 중 분석됨
- 현재 C3는 컴파일 타임 folding을 넘어서는 정적 분석은 수행하지 않음
- C3 매크로는 C 매크로처럼 컴파일 타임에 평가될 수 있지만,
macro키워드를 사용하고 C의 줄 끝\문제를 피함 - 매크로 파라미터에는 특수 접두사가 있음
$는 컴파일 타임에 알려진 값임#는 아직 평가되지 않은 식이며, 변수 전달 시 한 번 평가된 뒤 참조 위치에 직접 사용됨$나#파라미터를 쓰는 사용자 정의 매크로는@접두사가 필요함
- 컴파일 타임 변수만 가진 매크로는 완전히 컴파일 타임에 평가된다는 보장이 있음
- 타입 리플렉션과 컴파일 타임 실행은 매크로를 통해 처리됨
- C3 매크로는 C의 AST 조작 수준의 힘은 덜하지만, Zig의 comptime과 C 매크로의 혼합처럼 느껴짐
리터럴과 primitive 타입
- C3는 base64와 hex 데이터 리터럴을 지원함
b64"SGVsbG8gV29ybGQh"x"4865 6c6c 6f20 776f 726c 6421"
- 일반적인 사용 사례는
$embed매크로로도 충족될 수 있어, 자주 쓰이지 않는 기능에 문법 설탕을 추가한 느낌이 있음 - 문자열 전반은 C와 Zig처럼 “문자열은 바이트”라는 접근을 선호하는 입장에서는 다소 조심스럽게 느껴짐
- primitive 타입은 정수, bool, float 등을 포함하며 C와 대체로 비슷함
char는 unsigned로 정의되어 C처럼 컴파일러에 signedness가 맡겨지지 않음int128,uint128도 제공됨iptr,uptr,isz,usz는 짧지만 낯선 사람에게는 읽기 비용이 있을 수 있음- 다른 primitive 타입의 비트 크기가 명확하게 정의되어 C보다 개선된 점으로 평가함
설치와 프로젝트 생성 경험
- C3 웹사이트는 컴파일러 바이너리 설치 방법과 소스 빌드 방법을 제공함
- Gentoo 환경에서는 C3 패키지를 찾지 못해 소스 빌드를 시도함
git clone https://github.com/c3lang/c3c.gitcmake ..make
- 빌드 과정에서 CMake가
LLD_COFF,LLD_COMMON,LLD_ELF,LLD_MACHO,LLD_MINGW,LLD_WASM을 찾지 못함 - 설치된
lld 20.1.5에는 C3가 의존하는 LLD 라이브러리가 포함되지 않아 문제가 발생했고, 결국 사전 빌드된 C3 컴파일러를 사용함 - 사용한 컴파일러는
C3 Compiler Version: 0.7.1, LLVM backend, LLVM17.0.6, targetx86_64-pc-linux-gnu로 표시됨 c3c init hello_world는 프로젝트 디렉터리와LICENSE,README.md,project.json,src/main.c3등을 생성함LICENSE와README.md는 비어 있음- 빈
LICENSE파일은 일반 permissive license가 들어 있을 것으로 기대했다면 놓치기 쉬운 점임
c3c run은 프로젝트를 빌드하고 실행해Hello, World!를 출력함c3c init,c3c build,c3c compile흐름은 Zig의zig init,zig build,zig build-exe를 떠올리게 함
계산기 구현에서 드러난 실사용 감각
- 계산기 예제는
+,-,*,/, 단항-,^, 괄호를 처리하는 기본 계산기를 목표로 함 - 사용자 입력은
io::treadline()으로 받고, 반환값은String?optional로 처리함 - C3에는 기본 할당자로
tmem과mem이 있으며,tmem은 arena allocator처럼 동작하는 임시 할당자임@pool()범위 안에서tmem으로 할당한 항목은 스코프 종료 시 해제됨- 표준 라이브러리의 여러 함수는
t접두사로 임시 할당자 사용을 나타냄
- 이 방식은 Zig처럼 allocator를 명시적으로 넘기는 관행과 C처럼 표준 allocator를 쓰는 관행의 중간에 가까움
main이int를 반환하면 C3는 끝에서 값을 반환하도록 요구함- 반환이 빠지면 “Missing return statement at the end of the function” 오류가 발생함
main을void로 표시하는 것도 가능함
io::printf와io::printfn은 반환값을 반드시 처리하지 않아도 되며, 이는@maydiscardannotation 때문임- 반대 annotation으로
@nodiscard도 있음
- 반대 annotation으로
토크나이저 구현에서 본 장단점
- 토크나이저는 입력 문자열을
Token[]로 변환하며,TokenTagenum으로ADD,SUBTRACT,MULTIPLY,DIVIDE,POWER, 괄호,NUMBER,DELIMITER를 정의함 - 숫자는 문자열에서 숫자와
.을 스캔한 뒤to_float()로 변환함 - 표준 라이브러리의
std::collections::list와List {Token}은 토큰을 누적하는 데 편리함 - 익숙한 C 스타일
for루프와foreach루프가 함께 잘 작동함for는 스캔 로직의 중심에 쓰임foreach는 토큰 출력에 쓰임
- 언어 서버는 표준 라이브러리 코드 자동완성과 정의 이동이 작동하지 않아 작성 중 어려움이 있었고, 언어가 성숙하면 개선될 부분으로 봄
- 슬라이싱 문법은
some_string[start..end]형태이며 시작과 끝 인덱스가 모두 inclusive임- 대부분의 다른 언어와 달라
end - 1조정이 필요함 - 길이 0 slice를 이 방식으로 만들 수 없어 혼란스러움
- 이후
some_string[start:length]형태의 slice-by-length 문법이 있다는 점을 확인함
- 대부분의 다른 언어와 달라
tokenize함수 안에서 전달받은 allocator와 동시에tmem을 사용할 수 있어, 중간 처리와 반환값 할당을 분리하기 좋음
파서 구현과 오류 전파
- 파서는 재귀 하강 파서(recursive descent parser)로 작성됨
Parser구조체는Token[] source와usz index를 가지고,Parser.parse,Parser.expression,Parser.term,Parser.factor,Parser.negation,Parser.number메서드로 문법을 나눔- 문법은 다음 형태를 따름
<expression> ::= <term> (("+" | "-") <term>)*<term> ::= <factor> (("*" | "/") <factor>)*<factor> ::= <negation> ('^' <factor>)*<negation> ::= "-" <negation> | <number><number> ::= '(' <expression> ')' | <floating point number>
- 오류는
faultdef UNEXPECTED_TOKEN과 optional 반환으로 처리함 p.term()!,p.factor()!같은 재전파 덕분에 오류가 없는 코드처럼 작성하면서 잘못된 입력은 바깥으로 전파됨nextcase는expression과term에서 반복적인+,-,*,/처리를 깔끔하게 만드는 데 잘 맞았음- 예제 입력
2+3 * (-7/-4) ^ 3.14는Result: 19.388445를 출력함
최종 평가
- C3는 C의 맛을 유지하지만 같은 뒷맛은 아니며, 작업 자체는 재미있음
- C++와 Rust보다 단순하고, Rust, C++, Zig보다 개발 속도가 빠르며, C보다 안전하고 표현력이 높다고 평가함
- 매일 사용할 언어로 선택할 가능성은 크지 않고, 개인적으로는 Zig 작업을 더 선호함
- C로 작성될 프로젝트의 대안으로 C3를 사용할 수 있으며, 짧은 사용만으로도 잠재력을 보았고 계속 사용할 의향이 있음
- C3 표준 라이브러리 소스는 읽기 쉬웠고, 매크로 시스템은 다른 매크로 시스템보다 선호함
- 여전히 마음에 들지 않는 부분으로 양끝 포함 슬라이싱 문법과 오류 시스템이 남음
- C3를 시도하려는 사람에게는 더 좋아질 때까지 기다리기보다 직접 써보는 쪽을 권함