Haskell 계열의 ‘작고 재미있는 언어’들이 어떻게 구현되는지, 어떤 설계 선택들이 있으며, 전형적인 컴파일러 단계들이 무엇인지 살펴본다.
나는 여전히 그 lil' fun langs에 대해 생각하고 있다. 그건 어떻게 동작할까? 안에는 뭐가 들어 있을까? 내 췌장이 필요할까? 내 IR을 정규화하고 싶지 않다면? 게으름은 미덕일까?
Haskell-스러운 언어들은 비슷해 보일 수 있지만, 많은 차원에서 서로 다르다:
대부분의 구현은 표준적인 컴파일 단계들을 사용한다:
엄격 평가에서는 인자들이 함수에 전달되기 전에 평가된다. 지연 평가에서는 인자 값이 실제로 필요할 때만 평가되며; 결과는 캐시되므로, 작업은 최대 한 번만 발생한다.
-- lazy eval returns `3` without applying `foo`
length [ 1, foo 2, 4 ]
| 측면 | 엄격(ML, OCaml) | 지연(Haskell) |
|---|---|---|
| 정규화 | ANF / K-정규형 | STG / thunk 필요 |
| 클로저 변환 | 표준 평평한 클로저 | 클로저 + thunk + 업데이트 프레임 |
| 코드 생성 | 직관적 | eval/apply 또는 push/enter 필요 |
| 메모리 관리 | 값은 항상 평가됨 | 평가되지 않은 thunk를 포함할 수 있음 |
| 꼬리 호출 | 단순(점프) | 복잡(enter, update) |
| 디버깅 | 쉬움(콜 스택이 의미가 있음) | 어려움(thunk가 제어 흐름을 가림) |
| 런타임 복잡도 | 더 단순(~200 LOC C) | 더 복잡(~500–2000 LOC C) |
엄격 평가는 단순한 선택이다. 지연성을 원한다면, Peyton Jones의 STG 머신이 표준 접근이다. MicroHs는 STG 머신을 피하고, 그래프 축약을 동반한 조합 논리로 직접 컴파일함으로써 우회한다.
지연 평가는 또한 무한 컬렉션을 가능하게 한다 — 무한 리스트를 정의하고 필요한 만큼만 소비할 수 있다.
| 스타일 | 예시 | 구현 비용 |
|---|---|---|
| 커리드 | Haskell, Ben Lynn, MicroHs | 조합자 백엔드에서는 공짜; 네이티브 백엔드는 인자마다 클로저를 할당하지 않도록 arity 분석이 필요 |
| 밋밋 | MinCaml, OCaml(내부적으로), Grace, EYG | 더 단순한 코드 생성 -- 다인자 함수는 튜플 또는 여러 파라미터를 받는 함수일 뿐 |
커리드 언어에서 f x y는 ((f) x) y이다: 함수 적용 두 번. 백엔드가 f가 항상 인자 두 개를 받는다는 것을 감지하지 못하면(arity 분석), 다인자 호출마다 힙 할당 비용을 치르게 된다.
나는 기타를 치는 법을 독학하려 했다. 하지만 나는 끔찍한 선생님이다 — 왜냐하면 나는 기타 치는 법을 모르기 때문이다.
대부분의 컴파일러는 기존 언어(예: C, Rust, Haskell, OCaml)로 작성되며, 파싱 라이브러리, 빌드 도구, 패키지 관리 같은 호스트의 생태계에 기대어 있다.
부트스트랩 컴파일러는 자기 자신을 컴파일한다. 컴파일러를 그 언어로 작성한 뒤, 이전 버전의 컴파일러(또는 최소 시드 런타임)를 사용해 다음 버전을 빌드한다. 언어가 자급자족하게 되고; 컴파일러는 스스로의 테스트 스위트가 된다.
공부할 만한 훌륭한 셀프-호스티드 언어들이 많다:
C runtime (350 LOC)
→ compiler₁: lambda calculus + integers
→ compiler₂: + let, letrec, ADTs
→ compiler₃: + type inference
→ compiler₄: + pattern matching
→ compiler₅: + type classes
→ ...
→ compilerₙ: near-Haskell-98
인터프리터는 AST를 순회하거나 바이트코드를 단계적으로 실행하면서 프로그램을 직접 실행한다. 컴파일러는 프로그램을 다른 언어(예: x86, C, JS)로 번역하고, 실행은 그 타깃이 처리하게 한다.
여기서 경계는 흐릿하다. 바이트코드 VM은 중간 형태로 컴파일한다. “트랜스파일러”는 기계어가 아니라 소스 코드로 컴파일한다.
| 전략 | 예시 | LOC 추정 | 트레이드오프 |
|---|---|---|---|
| 트리-워크 인터프리터 | PLZoo poly, Eff, Frank, Grace, 1ML | 50–200 | 가장 단순. 코드 생성도 런타임도 없음. 느림(네이티브 대비 10–100×) |
| 바이트코드 VM | OCaml(ZINC), Tao, PLZoo miniml | 200–500 | 중간 지점. 이식성, 적당한 속도. ~30–50개 명령어 작성 |
| 네이티브 컴파일 | MinCaml, mlml, AQaml | 500–1500 | 빠른 실행, 하지만 레지스터 할당, 호출 규약, ABI를 직접 책임져야 함 |
| C로 트랜스파일 | Koka, Scrapscript, Chicken, Austral | 200–500 | 양쪽 장점 -- 이식 가능한 네이티브 속도, C 컴파일러가 어려운 부분을 담당 |
| JS/Go로 트랜스파일 | Newt, SOSML, Borgo | 200–400 | 웹/생태계 배포, 하지만 타깃의 성능 모델을 물려받음 |
| 조합자 축약 | Ben Lynn, MicroHs | 100–300 | 클로저도 레지스터도 없음. C로 된 그래프 축약 평가기. 단순하지만 느림 |
lil' fun langs는 보통 인터프리터다. 컴파일이 없다면 클로저 변환, 레지스터 할당, 런타임 시스템을 건너뛸 수 있다. 인터프리터에서 컴파일러로의 도약은 ~500–2000 LOC 정도의 비용이 든다.
type Meters = Int
type Seconds = Int
-- Nominal: Meters ≠ Seconds (different names)
-- Structural: Meters = Seconds (same shape)
| 스타일 | 예시 | 결과 |
|---|---|---|
| 이름 기반 | OCaml, Haskell, Austral | 이름이 정체성 -- 모양이 같다고 같은 타입이 아님 |
| 구조적 | EYG, Grace, TypeScript, Simple-sub | 모양이 정체성 -- 같은 필드/변형이면 같은 타입 |
대부분의 ML 계열 언어는 대수적 데이터 타입에는 이름 기반이지만, (구현되어 있다면) 레코드에는 구조적인 경우가 많다. 행 다형성(EYG, Grace, Koka)은 본질적으로 구조적이다 -- “적어도 이 필드를 가진 어떤 레코드든”에 대해 동작한다. Simple-sub는 더 나아가 합집합/교집합 타입을 제공하면서도 주(Principal) 추론을 유지한다.
-- Ugly:
Error: type mismatch: int vs string
-- Pretty:
3 │ let x = 1 + "hello"
│ ^^^^^^^^
Error: I expected an `int` here, but got a `string`.
The left side of `+` is `int`, so the right side must be too.
예쁜 에러는 겉에 페인트칠만 한다고 얻어지지 않는다. 코드의 특정 줄/영역을 가리키려면, 모든 컴파일 단계에 걸쳐 소스 위치를 스레딩해야 한다. 최소로 쓸 만한 에러 시스템은 다음을 포함한다:
{ file, start_line, start_col, end_line, end_col }를 가진다. 이는 노드당 필드 하나가 추가되는 비용이다.where를 let으로 내릴 때, 새 let 노드는 where의 스팬을 상속한다.| 품질 | 예시 | 비용 |
|---|---|---|
| Elm급 | Elm, Austral | 실패 모드마다 목적 맞춤형 에러 메시지. 노력 최대, UX 최고 |
| 충분히 좋음 | Tao, Ante, OCaml | 소스 스팬 + 일반 포매팅. 90% 케이스 커버 |
| 위치만 | MinCaml, 대부분의 작은 컴파일러 | 줄 번호는 있지만 스팬 하이라이트나 설명이 없음 |
| De Bruijn 인덱스 | Elaboration Zoo(의도적으로) | 변수 이름이 사라짐 -- 연구에는 괜찮지만 사용자에게는 나쁨 |
| 접근 | 사용처 | LOC 추정 | 비고 |
|---|---|---|---|
| 손수 작성한 재귀 | MinCaml(Rust 포트), Tao, Ante | 100–300 | 완전한 제어, 최고의 에러 |
| ocamllex / mlllex | MinCaml(원본), HaMLet, PLZoo | 50–100 | OCaml/SML 호스트의 표준 |
| Alex(Haskell) | MicroHs, 많은 Haskell-호스티드 | 50–100 | Haskell 호스트의 표준 |
| 파서 콤비네이터(통합) | Ben Lynn, 일부 교육용 | 0(파서의 일부) | 렉서 없는 파싱 |
선택적 향상:
"hello ${name}" 같은 문법은 ML 계열의 표준은 아니지만, 일부 새 언어가 추가한다.파싱은 평평한 토큰 스트림을 트리로 변환한다. 표면 문법은 구체 구문 트리(CST)로 파싱되거나, 곧바로 추상 구문 트리(AST)로 변환된다. ML 계열 언어는 문법이 잘 정리되어 있고 거의 LL(1)이다.
| 접근 | 사용처 | LOC 추정 | 비고 |
|---|---|---|---|
| 재귀 하강 + Pratt/우선순위 클라이밍 | MinCaml(Rust 포트), Tao, Ante | 200–500 | 최고의 에러 메시지, 확장하기 가장 쉬움 |
| ocamlyacc / mlyacc(LALR) | MinCaml(원본), HaMLet | 100–200(문법 파일) | 표준이지만 에러 복구가 나쁨 |
| 파서 콤비네이터(Parsec 스타일) | Ben Lynn, MicroHs, PLZoo | 100–400 | 우아함, 조합성, 백트래킹 |
| PEG / Packrat | ML 계열에서는 드묾 | 100–300 | 선형 시간 보장 |
그 다음 단계들은 모두 이 타입을 변환한다. ML 계열 언어에서 AST는 보통 이렇게 생겼다:
type expr =
| Lit of literal (* 42, 3.14, "hello", true *)
| Var of name (* x *)
| App of expr * expr (* f x *)
| Lam of name * expr (* fun x -> e *) (or \x -> e)
| Let of name * expr * expr (* let x = e1 in e2 *)
| LetRec of name * expr * expr (* let rec f = e1 in e2 *)
| If of expr * expr * expr (* if c then t else f *)
| Tuple of expr list (* (a, b, c) *)
| Match of expr * branch list (* match e with p1 -> e1 | ... *)
| Ann of expr * type (* (e : t) *)
타입 추론 전에, 표면 AST는 단순화된다:
where 절 → let
* 패턴 매칭의 가드 → 중첩 if
* do 표기(모나딕) → >>= 체인
* 리스트 컴프리헨션 → concatMap
* 연산자 섹션 → 람다: (+ 1)는 fun x -> x + 1
* 레코드 문법 → 위치 기반 생성자 + 접근자 함수
* 타입 클래스 인스턴스 → 딕셔너리 전달(엘라보레이션)이는 ML 계열 언어의 심장이다. “표준” 알고리즘은 Hindley-Milner(HM) 타입 추론이며, 특히 Algorithm W 또는 Algorithm J다.
핵심 구성요소:
type ty = TVar of tvar | TCon of string | TArr of ty * ty | TTuple of ty listlet 경계에서, 타입 안의 자유 타입 변수를 전칭 양화하여 다형적 타입 스킴을 만든다: ∀α. α → α.-- Given:
let id = fun x -> x in (id 1, id true)
-- Type inference trace:
-- 1. id : α → α (infer: x has fresh type α, body is x)
-- 2. generalize: id : ∀α. α → α (α is free at let boundary)
-- 3. id 1: instantiate α=β, unify β→β with int→γ, get int
-- 4. id true: instantiate α=δ, unify δ→δ with bool→ε, get bool
-- 5. result: (int, bool)
| 접근 | 사용처 | LOC 추정 | 비고 |
|---|---|---|---|
| Algorithm W(치환 기반) | Algorithm W Step-by-Step, PLZoo | 150–400 | 이해하기 가장 쉬움, 치환을 즉시 합성 |
| Algorithm J(가변 참조) | MinCaml, 대부분의 프로덕션 컴파일러 | 100–300 | 더 효율적, 가변 유니피케이션 변수를 사용 |
| 제약 기반(HM(X)) | GHC, 일부 연구용 컴파일러 | 500–2000 | 제약 생성과 해결을 분리; 확장 가능 |
| 양방향 타입 체크 | Elaboration Zoo, 일부 의존 타입 시스템 | 200–500 | 체크/추론 모드를 번갈아 수행; 의존 타입으로 확장됨 |
하지만 화려한 타입 시스템 기능은 공짜가 아니다:
| 향상 | 추가 복잡도 | 사용처 |
|---|---|---|
| 타입 클래스/트레이트 | +500–2000 LOC | Haskell, MicroHs, Ben Lynn(후기 단계), Tao |
| 행 다형성(확장 가능한 레코드/변형) | +300–800 LOC | Koka, 1ML, EYG, Grace |
| 고차 종류 타입 | +200–500 LOC | Haskell, Koka |
| GADT | +500–1500 LOC | GHC, OCaml 4.x+ |
| 대수적 이펙트(타입드) | +500–1500 LOC | Koka, Eff, Frank |
| 의존 타입(완전한) | +1000–5000 LOC | Elaboration Zoo, Idris, Lean |
| 대수적 서브타이핑(합집합/교집합) | +500 LOC | Simple-sub, MLscript |
| 일급 다형성(System F) | +300–1000 LOC | 1ML, MLF |
| 모듈 시스템(펑터, 시그니처) | +1000–5000 LOC | HaMLet, OCaml, 1ML |
다른 전략들:
∀ 양화 없음). 모든 타입이 완전히 결정된다. 이는 일반화와 인스턴스화를 완전히 제거해 타입 체커를 ~100 LOC로 줄인다. id x = x 같은 함수는 각 사용 지점에서 구체 타입을 갖게 된다.sort :: Ord a => [a] -> [a]는 sort :: OrdDict a -> [a] -> [a]가 된다. Ben Lynn의 컴파일러와 MicroHs는 둘 다 이 접근을 사용한다.타입이 추론되면, 패턴 매칭을 효율적인 결정 트리 또는 케이스 트리로 컴파일할 수 있다.
| 접근 | 사용처 | LOC 추정 | 비고 |
|---|---|---|---|
| 결정 트리(Maranget 알고리즘) | 대부분의 현대 컴파일러, Tao, Ante | 200–600 | 최적 -- 중복 테스트 없음, 좋은 코드 |
| 백트래킹 오토마타 | 오래된 컴파일러, 단순 구현 | 100–300 | 더 단순하지만 작업이 중복될 수 있음 |
| 중첩 if/switch(나이브) | 많은 교육용 컴파일러 | 50–100 | 정답이지만 최악의 경우 지수적으로 나쁨 |
| 완전히 생략 | MinCaml, PLZoo poly | 0 | 프리미티브에 대한 if/then/else만 지원 |
| 디펑셔널라이즈드 | 일부 교육용 컴파일러 | 50–150 | 폴스루가 있는 부분 함수들의 시퀀스; 더 단순하지만 덜 효율적 |
핵심 단계:
(Cons (x, Cons (y, Nil))) → 테스트들의 시퀀스.정전(定典) 같은 참고문헌은 Compiling Pattern Matching to Good Decision Trees다. Luc Maranget의 알고리즘은 테스트 수 관점에서 증명 가능한 최적의 결정 트리를 만든다. OCaml과 Rust가 이 접근을 사용한다.
-- Before (nested expression):
f (g x) (h y)
-- After (A-normal form):
let a = g x in
let b = h y in
f a b
모든 중간 값이 이름을 갖는다. 모든 함수 인자는 사소한(trivial) 형태가 된다. 평가 순서는 이제 let 체인에서 명시적이다.
정규화 전략:
| 전략 | 사용처 | 성격 |
|---|---|---|
| K-정규형(MinCaml의 ANF 변형) | MinCaml 및 파생들 | 직접 스타일; let으로 모든 중간값에 이름 부여 |
| A-정규형(ANF) | Flanagan 외 1993, 많은 현대 컴파일러 | 사실상 K-정규형과 동일; 표준 명칭 |
| 연속 전달 스타일(CPS) | Appel의 SML/NJ, Rabbit, CertiCoq | 모든 함수가 추가 continuation 인자를 받음; 모든 호출이 꼬리 호출 |
| 정규화 없음 | Ben Lynn | 타입드 AST → 조합 논리로 직접. 그래프 축약에는 되지만 네이티브 코드 생성에는 부적합 |
| SSA 직접 | Scrapscript | ANF/CPS를 건너뜀; SCCP + DCE가 있는 SSA IR. 나머지는 LLVM/C가 처리 |
| 모나딕 정규형 | 일부 의존 타입 시스템(Bowman, 2024) | ANF와 비슷하지만 let 대신 모나딕 bind 사용; 특정 최적화에 더 깔끔 |
프로그램이 정규형에 있으면, 최적화 패스가 이를 단순화할 수 있다. 작은 컴파일러에서는 최적화를 최소로 유지한다 -- 목표는 GCC와 경쟁하는 것이 아니라, 민망하지 않을 정도로만 느리지 않게 하는 것이다.
MinCaml의 최적화 패스(총 ~300 LOC):
| 패스 | LOC(MinCaml) | 효과 |
|---|---|---|
| 베타 축약 | ~50 | let x = y in ... x ... → ... y ... 인라인 |
| let 평탄화(결합) | 22 | let x = (let y = e1 in e2) in e3 → let y = e1 in let x = e2 in e3 |
| 인라인 확장 | ~100 | 작은 함수 호출을 본문으로 대체 |
| 상수 폴딩 | ~50 | 3 + 4 → 7 |
| 죽은 코드 제거 | ~50 | x가 e2에서 자유 변수가 아니면 let x = e1 in e2 제거 |
| 공통 부분식 제거 | ~50 | (MinCaml에서는 옵션, 해시-콘싱으로) |
이 여섯 패스는 작은 컴파일러에서 최적화 가치의 80%+를 커버한다. 고정점에 도달할 때까지 반복 적용된다(보통 2–3회 반복).
기본을 넘어:
| 최적화 | 복잡도 | 효과 |
|---|---|---|
| 꼬리 호출 최적화 | +50–100 LOC | 함수형 언어에 필수; 루프는 재귀 호출 |
| 알려진-호출 최적화 | +50 LOC | 호출 대상이 정적으로 알려지면 클로저 간접 참조를 건너뜀 |
| 언박싱(특수화) | +200–500 LOC | 다형 함수의 단형 사용에서 박싱 회피 |
| 컨티피케이션 | +100–300 LOC | 항상 꼬리 위치에서 호출되는 함수를 로컬 점프로 변환 |
| 수요 분석(엄격성) | +500–2000 LOC | 지연 언어용: 항상 평가되는 인자 판별 |
| worker/wrapper 변환 | +200–500 LOC | 엄격 인자와 지연 인자를 분리해 코드 생성 개선 |
| 디포레스트레이션/퓨전 | +500–2000 LOC | 중간 자료구조 제거(예: map f . map g → map (f . g)) |
| 전체 프로그램 최적화 | 다양 | JHC는 GRIN으로 이를 수행; 미사용 생성자 제거, 전역 특수화 |
-- Before:
let f = \ x -> x + y
-- After:
let f =
{ fun = \ env x -> x + env.y
, env = { y = y }
}
최적화된 IR에는 여전히 자유 변수를 가진 함수들이 있다. 클로저 변환은 모든 함수를 “닫힌(closed)” 형태로 만든다 -- 하드웨어는 어휘적 스코프를 이해하지 못하기 때문이다. 모든 함수는 (코드 포인터, 환경 레코드) 쌍이 된다. 환경은 정의 지점에서 함수의 자유 변수를 캡처한다.
| 접근 | 사용처 | 트레이드오프 |
|---|---|---|
| 평평한 클로저 | MinCaml, OCaml, 대부분의 컴파일러 | 환경은 캡처된 값들의 평평한 벡터. O(1) 접근, 클로저당 1회 할당. 표준 선택. |
| 연결/공유 클로저 | 일부 오래된 Scheme 컴파일러 | 환경은 프레임들의 연결 리스트. 클로저들 사이 구조 공유. 할당 더 많고 접근 느림. |
| 람다 리프팅 | GHC(선택적으로), 일부 교육용 컴파일러 | 추가 파라미터를 붙여 클로저를 완전히 제거. 클로저 자체에 힙 할당이 없음. 하지만 호출자는 더 많은 인자를 전달해야 하고, 호출 지점을 갱신해야 함. |
| 디펑셔널라이제이션 | Reynolds(1972), MLton | 고차 함수를 합 타입에 대한 1차 디스패치로 대체. 함수 포인터를 완전히 제거. 전체 프로그램 분석이 필요. |
| 조합 논리(브래킷 추상화) | Ben Lynn, MicroHs | 람다를 SKI 조합자(또는 변형)로 대체. 클로저도 환경도 없음. 그래프 축약으로 평가. |
코드 생성은 타깃 선택에 의해 완전히 결정된다:
| 타깃 | 사용처 | LOC 추정 | 트레이드오프 |
|---|---|---|---|
| 네이티브 어셈블리(x86-64, ARM 등) | MinCaml, mlml, AQaml | 300–800 | 최고 성능, 가장 많은 작업, 플랫폼 종속 |
| C 소스 | Koka, Scrapscript, Chicken, JHC, Austral | 200–500 | 이식성, C 컴파일러 최적화 활용, 하지만 간접화 |
| LLVM IR | Ante, gocaml, Harrop의 MiniML | 200–500 | 좋은 네이티브 성능, 크로스플랫폼, 하지만 큰 의존성 |
| Cranelift | MinCaml(Rust 포트), 일부 새 언어 | 200–500 | LLVM보다 빠른 컴파일, 좋은 코드 생성, Rust 네이티브 |
| 바이트코드(커스텀 VM) | OCaml(ZINC 머신), PLZoo miniml | 200–500 | 이식성, 단순함, 하지만 실행이 느림 |
| JavaScript / Wasm | MinCaml-wasm, SOSML, Newt, 기타 | 200–400 | 웹 배포, 하지만 제한된 성능 모델 |
| Go 소스 | Borgo | 200–500 | Go의 생태계/툴링/동시성 모델을 상속 |
| 조합 논리 | Ben Lynn, MicroHs | 100–300 | 레지스터 할당이 필요 없지만 실행이 느림 |
| 정규화기(런타임 타깃 없음) | Dhall | 200–500 | “컴파일” = 정규형으로 줄이기. 실행 파일 출력 없음 |
프로그램은 변수들을 임의로 많이 사용하지만, CPU 레지스터 수는 고정되어 있다. 레지스터 할당은 어떤 변수가 레지스터에 살고, 어떤 것이 메모리로 스필되는지 결정한다.
네이티브 어셈블리를 타깃으로 한다면 이를 직접 구현한다. C/LLVM/Cranelift 등을 타깃으로 하면 백엔드가 이를 처리한다.
| 접근 | 사용처 | LOC 추정 | 품질 |
|---|---|---|---|
| 그래프 컬러링(Chaitin-Briggs) | MinCaml, Appel의 교과서 | 200–500 | 대부분의 경우 최적, 표준 |
| 리니어 스캔 | 일부 JIT, 단순 컴파일러 | 100–200 | 컴파일이 빠르고 코드 품질이 약간 나쁨 |
| 나이브(전부 스필) | 일부 교육용 컴파일러 | 50 | 정답이지만 성능이 끔찍함 |
| 해당 없음 | C/LLVM/바이트코드 타깃 컴파일러 | 0 | 백엔드에 위임 |
최소 구성은 다음을 포함한다:
| 구성요소 | 복잡도 | 비고 |
|---|---|---|
| 엔트리 포인트/스택 셋업 | 10–30 LOC C | 초기 힙/스택 포인터 설정 |
| 가비지 컬렉터 | 100–1000 LOC C | 아래 참고 |
| 프리미티브 연산 | 50–200 LOC C/asm | I/O, 수학, 문자열 연산 |
| 할당 루틴 | 10–50 LOC | 범프 할당기(수집은 GC가 처리한다면) |
| 클로저 표현 | 코드 생성의 일부 | 클로저가 메모리에 어떻게 배치되는지 |
Lil' fun langs는 자주 할당한다 -- 모든 클로저, 모든 cons 셀, 모든 부분 적용. 회수가 없으면 금방 메모리가 바닥난다. 가비지가 쌓이지 않게 해야 한다:
| 전략 | 사용처 | 복잡도 | 비고 |
|---|---|---|---|
| GC 없음(메모리 누수) | 일부 교육용 컴파일러, MinCaml 벤치마크 | 0 | 짧게 실행되는 프로그램에는 가능 |
| Cheney 복사(세미스페이스) | 많은 작은 컴파일러, Appel의 교과서 | 100–300 LOC C | 단순하고 빠르지만 2× 메모리 사용 |
| 마크-스윕 | 다양 | 100–300 LOC C | 객체를 이동시키지 않음, 포워딩 불필요 |
| 참조 카운팅 | Koka(Perceus), Carp, Swift-유사 | 200–500 LOC | 정지 시간이 없음; Perceus는 컴파일 타임 삽입으로 오버헤드 없이 정밀하게 달성 |
| 리전 기반 | MLKit, 일부 연구 언어 | 300–1000 LOC | 컴파일 타임 메모리 관리, GC 정지 없음 |
| 아레나/스택 전용 | 아주 단순한 컴파일러 | 20–50 LOC | 아레나에 할당하고 한 번에 전부 해제 |
| 소유권/어파인 타입 | Rust, Carp, Lean 4 | 0(컴파일 타임) | 런타임 GC가 필요 없지만 언어를 제한함 |
언어에 대수적 이펙트(Eff, Frank, Koka, Ante)가 있다면, 런타임은 제한된 연속(delimited continuations) 또는 CPS로 변환된 호출 규약을 지원해야 한다. 이펙트 핸들러는 본질적으로 연속을 캡처하기 위해 두 번째 스택 또는 세그먼트 스택이 필요하다. Koka는 이를 evidence-passing으로 처리하고; Eff와 Frank는 인터프리팅을 사용한다.