Jam 프로그래밍 언어
1 week ago
8
Jam 은 C 계열 언어의 즉각적인 사용감을 유지하면서, GC 없이 안전성·낮은 학습 곡선·고성능을 함께 노리는 v1.0 이전 단계의 언어임
핵심은 mutable value semantics 와 Rust식 drop 시스템으로, 사용자 코드에 참조나 lifetime 문법을 노출하지 않고 소유권·borrow·자동 정리를 컴파일러가 처리함
초기화 모델은 undefined와 암묵적 zero 초기화를 모두 피하고, 지연 초기화와 out-parameter는 Maybe(T) 및 unsafeAssumeInit() 분석으로 다룸
export는 Jam 함수를 C ABI 로 노출하고 Jam struct는 C 호환 layout을 갖도록 설계되어, 별도 unsafe shim이나 repr annotation 부담을 줄이려는 방향임
컴파일러는 아직 C++로 구현된 부트스트랩 단계이며 공개 전이고, 108개 distinct project를 Jam으로 만든 뒤 오픈소스화할 계획임
Jam이 겨냥하는 언어 위치
Jam은 아직 v1.0 이전 이며, 현재 설명된 메커니즘은 컴파일러에서 동작하지만 안정화 전 세부사항은 바뀔 수 있음
목표는 Go, Zig, modern C처럼 바로 이해하기 쉬운 C 계열 감각 을 유지하면서 C의 bug class를 줄이는 안전한 언어를 만드는 것임
설계의 중심축은 두 가지임
Racordon, Abrahams et al. 2022의 Mutable value semantics
Rust의 drop system
실제 팀은 숙련도가 섞여 있고 덜 숙련된 구성원이 실수할 가능성이 크기 때문에, 언어가 리뷰 전에 더 많은 오류를 막아야 한다는 문제의식에서 출발함
Rust, Zig, C++와의 차이
Rust는 안전성 철학이 강하지만, “Rust를 어느 정도 쓸 수 있음”과 “Rust로 생산적임” 사이의 간극이 커 팀의 학습 곡선이 부담이 될 수 있음
Zig는 C-like 언어에 가까운 작은 표면적과 즉각적인 mental model을 주지만, 언어 차원에서 안전한 언어는 아님
uninitialized read, manual cleanup, use-after-free 방지가 언어 수준에서 강제되지 않음
큰 Zig 또는 C++ production 프로젝트는 Valgrind, AddressSanitizer, fuzzing 같은 검증 도구에 크게 의존함
AI 시대에는 production code의 많은 부분이 사람이 아닌 도구로 작성되거나 초안 작성되며, 병목이 code writing에서 code review 로 이동한다고 봄
code volume은 늘고 review surface는 flat하므로 compiler가 더 많은 버그를 잡아야 함
자동 drop 시스템
Jam의 binding은 값을 소유하고, drop-bearing type의 binding이 scope를 벗어나면 컴파일러가 drop 호출을 합성함
예시 File type은 fn drop(self: mut File)을 선언하고, useFile()에서는 const f: File = { fd: 7 };만 작성함
명시적 cleanup, defer, lifetime 종료 표시가 없음
LLVM IR에는 ret 직전에 call void @__drop_File(ptr %1)가 생성됨
mangled name인 __drop_File은 여러 type의 drop 함수가 LLVM level에서 충돌하지 않게 함
self: mut File은 pointer parameter로 lowering되고, call site는 binding 주소를 직접 전달함
Zig에서는 같은 cleanup을 위해 defer f.deinit()을 명시해야 함
해당 줄을 제거하면 IR의 deinit call도 사라짐
file descriptor leak은 programmer가 cleanup을 기억하지 못할 때 발생함
C++ RAII도 scope exit에서 destructor를 자동 실행하지만, Jam은 Rust의 단순한 drop 모델을 채택함
C++의 rule of 0/3/5, virtual destructor, constructor exception, destructor exception, std::exit, std::abort, longjmp, signal 같은 복잡성을 피하려는 방향임
Jam은 type당 하나의 drop function을 두고, 모든 scope exit에서 실행함
초기화와 Maybe(T)
Jam에는 undefined 값이 없고 , binding을 값 없이 선언할 수 없음
모든 var와 const는 실제 initializer를 요구함
struct는 field 값을 먼저 계산하고, struct literal로 생성한 뒤 binding함
Zig는 var f: File = undefined; return f.fd;를 허용하며 runtime에서는 stack garbage를 읽을 수 있음
Debug mode에서는 misuse가 보이도록 0xaa fill이 들어감
Release mode에서는 arbitrary bytes가 됨
Go는 모든 var를 zero-initialize해 garbage read를 막지만, 곧 overwrite될 field에도 zero pattern을 쓰는 비용이 있음
Jam은 undefined와 implicit zero 양쪽을 모두 피함
지연 초기화와 out-parameter에는 Maybe(T) 를 사용함
empty()는 아직 의미 없는 contents를 가진 slot을 만듦
write()는 slot을 채움
unsafeAssumeInit()은 값을 추출함
lint pass는 slot이 write되었는지 추적하고, analyzer가 초기화를 증명하지 못한 unsafeAssumeInit() 호출을 compile error로 거부함
unsafe prefix는 human과 AI reviewer가 grep할 수 있는 anchor로 남음
Scope exit, return, break, continue
compiler는 drop scope stack 을 추적하고 lexical block boundary마다 새 scope를 push함
block이 끝나거나 branch로 빠져나가기 직전에 해당 scope의 binding drop을 emit함
if, else, match arm, while, for body 안의 binding은 해당 block 끝에서 drop됨
nested block 안의 return은 실제 ret 전에 active scope를 innermost-first로 drop함
break와 continue는 loop body 안에서 열린 scope를 drop한 뒤 loop exit 또는 다음 iteration으로 이동함
nested break 예시에서는 outer가 iteration 0 끝에서 drop되고, iteration 1의 break path에서는 inner 다음 outer 순서로 drop됨
Parameter mode와 first-class reference 제거
함수 호출에서 binding이 drop되는지는 parameter mode 가 결정함
기본 mode는 read-only borrow임
callee가 값을 읽고 caller의 binding은 initialized 상태로 유지됨
call return 시 drop이 발생하지 않음
mut은 exclusive read-write borrow임
caller의 binding은 call 이후에도 initialized 상태로 남음
move만 값을 consume함
callee가 소유권을 받고 callee 끝에서 drop됨
caller의 binding은 call 이후 Uninit이 되며 읽으면 compile error임
call site marker는 없고, f(x) 형태는 모든 mode에서 같음
Jam에는 first-class reference type 이 없음
borrow를 variable에 저장하거나 return하거나 struct field에 보관할 수 없음
parameter borrow는 call-frame 동안만 존재하고 call return 시 만료됨
lifetime annotation이 필요하지 않은 이유는 attach할 lifetime이 없기 때문임
collection API도 value-shaped로 유지됨
v[i] = x는 v.setAt(i, x)로 desugar됨
let y = v[i]는 v.at(i) getter가 element를 value로 반환함
call site exclusivity check는 argument가 만든 borrow set의 path overlap을 검사함
swap(p.x, p.y)는 disjoint sub-path라 OK
moveX(p, p.x)는 p와 p.x가 overlap하므로 error임
C ABI와 FFI
Rust의 native ABI는 unstable이라 distribution boundary를 넘으면 C 형태로 다시 encoding해야 함
raw pointer dereference는 unsafe
ownership은 Box::into_raw와 Box::from_raw로 수동 전달됨
struct를 by value로 넘길 때는 #[repr(C)] 같은 별도 annotation이 필요함
cbindgen과 abi_stable 같은 도구는 이 경계의 수작업을 줄이기 위해 존재함
Jam은 first-class reference, lifetime, niche-packed layout이 없어 Jam value가 value-shaped all the way down이라고 봄
Jam struct는 이미 C-compatible layout 을 갖도록 설계됨
export는 Jam 함수를 C calling convention의 plain unmangled name으로 노출함
export fn counterAdd(c: mut Counter, n: i64) i64는 C에서 int64_t counterAdd(Counter *c, int64_t n);로 호출 가능함
mut Counter parameter는 caller-owned storage에 대한 Counter *로 lowering됨
Jam 쪽 함수 body는 ordinary Jam이라 drop, init analysis, call-site exclusivity rule이 계속 적용됨
C로 들어가는 방향은 extern으로 C signature를 선언함
extern function은 C ABI를 literal하게 따름
parameter-mode machinery는 boundary 밖에는 적용되지 않음
raw pointer로 C에 buffer를 넘기며, C가 pointer로 무엇을 하는지는 Jam이 검증하지 않음
Jam이 제공하려는 범위는 Jam 쪽이 safe by default로 유지되고, Jam library를 C ABI로 노출할 때 별도 unsafe API mirror나 shim layer를 만들지 않아도 되는 점임
Pattern matching
Jam의 match는 Pattern Block 형태이며 =>를 쓰지 않음
scrutinee는 match (opcode)처럼 괄호를 사용함
_는 catch-all arm임
arm은 top-to-bottom sequential first-match이고 implicit fallthrough가 없음
Game Boy emulator의 opcode dispatcher가 주요 사용 사례임
256 base opcodes와 256 prefix opcodes를 dispatch하는 형태임
enum payload matching도 지원함
variant pattern은 tag를 match하고 payload field를 arm 내부 fresh local로 bind함
compiler는 variant set에 대한 exhaustiveness를 검사함
새 variant를 추가하면 해당 variant를 다루지 않는 match site가 compile fail됨
match는 expression으로도 동작함
각 arm block은 trailing expression의 값을 생성함
모든 arm은 같은 type을 produce해야 함
match는 exhaustive해야 함
내부적으로 모든 match는 Luc Maranget 2008 기반의 결정 트리 pipeline 을 거쳐 compile됨
integer literal cascade는 LLVM simplifycfg가 수익성이 있을 때 switch와 jump table로 fold함
Compile time 설계
Rust compile pipeline은 여러 IR과 분석 단계를 거침
tokens → AST → HIR → THIR → MIR → monomorphization → LLVM IR → machine code
trait solving은 search problem이고, borrow checking은 whole-function region analysis임
monomorphization은 LLVM 이전 code volume을 늘림
Jam pipeline은 더 짧게 설계됨
tokens → AST → AstGen → JIR → codegen → LLVM IR → machine code
typed IR인 JIR 하나를 사용함
JIR은 AstGen이 만들 때부터 typed 상태임
Jam에는 untyped lowering을 강제하는 comptime-as-values가 없다고 봄
drop placement, init-before-use check, call-site exclusivity rule은 JIR 위의 local dataflow pass로 수행됨
type annotation이 binding마다 있기 때문에 global type inference와 open-ended trait search 부담이 적다고 봄
AST와 JIR은 flat data structure임
small fixed-size node를 contiguous array에 packing함
pointer 대신 index를 쓰고, oversized payload는 side pool에 저장함
compiler가 heap-allocated tree를 추적하는 대신 cache-friendly array를 순회하도록 함
backend에서는 LLVM이 release build optimization 시간을 지배함
debug build에는 Cranelift, release build에는 LLVM을 쓰는 split이 계획됨
Cranelift는 roadmap에 있으며 아직 완료되지 않음
현재 compiler는 C++ implementation으로 language를 bootstrap하는 단계이고, 인용할 만한 build-time benchmark는 아직 없음
compile-time 관련 claim은 측정 결과가 아니라 design claim임
Runtime performance와 예제
목표는 Jam이 Rust와 Zig에 performance를 맞추는 것임
Jam에는 GC, managed-memory runtime, per-allocation header가 없음
codegen은 straightforward LLVM IR임
아직 Rust와 Zig 수준에 도달했다고 보지는 않음
Rust와 Zig는 standard library의 target-specific intrinsic, auto-vectorization hint, allocator-aware container, hot path tuning, LLVM pass tuning 같은 작업을 오래 해옴
Jam도 마지막 10~30%를 좁히려면 같은 종류의 작업이 필요함
지금 측정한 workload에서는 gap이 “다른 class”가 아니라 small constant factor 안에 있다고 봄
terminal에서 실행되는 Tetris demo가 Jam으로 작성됨
공개 계획과 남은 작업
Jam은 아직 public이 아님
compiler는 존재하고 동작하지만 wider release 전임
day-to-day 사용성을 위해 다음 작업을 진행 중임
stable surface
package manager
LSP
formatter
나머지 tooling
별도 글로 다룰 예정인 주제가 남아 있음
parameter mode system
exclusivity rule
generics
Jam의 comptime
standard library
allocator systems
panic model
GPU codegen pipeline을 위한 MLIR exploration
Rust ABI work for FFI
Cranelift
self-hosted compiler 경로
오픈소스 계획은 Jam으로 108개 distinct project 를 만든 뒤 공개하는 것임
숫자 108은 Suikoden 2의 108 Stars of Destiny에서 온 arbitrary milestone임
현재는 small group of users에게 나갔고 tooling이 따라오면 범위를 넓힐 계획임
early access는 jamlang.org 의 beta list로 받을 수 있음
Homepage
Tech blog
Jam 프로그래밍 언어
🔉 볼륨 줄이기
🔊 볼륨 키우기
🔇 음소거
⏭️ 다음 곡