Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

1장: 소개

동기

제가 2014년에 이 글을 쓰기 시작한 것은 제게 필요한 입문서가 존재하지 않았기 때문입니다. 함수형 컴파일러에 관해 제가 찾은 자료는 대개 난해하고 다가가기 어려웠으며, 업계에서 소프트웨어를 만드는 사람들보다는 학계 독자를 위해 쓰인 것이었습니다. 저는 누군가가 동작하는 컴파일러를 함께 짚어 가며 필요한 시점마다 그 아이디어를 설명해 주기를 바랐습니다.

저는 제 지식의 빈틈을 천천히 채워 나갔지만, 다른 사람들까지 같은 느린 길을 걸어야 할 이유는 없었습니다. “보고 싶은 변화가 직접 되어라”라는 정신으로, 저는 제가 찾아 헤매던 입문서를 직접 쓰기로 했습니다. 이 책은 다음 사람을 위해 사다리를 내려 주는 제 나름의 방법입니다.

우리는 타입 추론, 대수적 데이터, 명목적 레코드, 타입 클래스를 갖춘 엄격한 함수형 언어인 Fun을 만들 것입니다. 그리고 Fun을 만드는 일은 분명 재미(fun)있을 것입니다! 우리는 이를 Rust로 작성하고 LLVM을 거쳐 네이티브 코드까지 끝까지 가져갈 것입니다. 그 과정에서 코드 생성기를 갖추기 전에도 프로그램을 실행할 수 있게 해 주는 인터프리터를 만들고, 진행하면서 우리의 변환을 검증할 것입니다. Haskell Report는 구문과 타입 체계에 대한 출발점을 제공합니다 (Simon Marlow 2010).

각 변환이 작은 입력 언어와 작은 출력 언어를 가지면 컴파일러를 이해하기가 더 쉬워집니다. 앞쪽 장들에서는 파싱, 평가, 추론을 각각 따로 살펴봅니다. 7장부터는 각 크레이트가 같은 컴파일러의 완전한 스냅숏입니다. 각 장은 해당 diff와 함께 읽으십시오. 더 단순한 구현을 다시 살펴보고 싶을 때는 언제든 이전 스냅숏을 실행하십시오.

도구 설정

Apple Silicon macOS나 x86-64 또는 AArch64 기반 Linux를 사용하십시오. 각 장을 빌드하려면 Rust와 Cargo가, 저장소 레시피를 실행하려면 just가, 런타임에는 C 컴파일러와 Boehm GC가, 네이티브 컴파일과 JIT에는 LLVM 22가 필요합니다. 책에는 just bootstrap으로 설치하는 별도의 툴체인이 있습니다.

macOS

Apple의 커맨드라인 개발 도구를 설치하고, brew를 아직 사용할 수 없다면 Homebrew 설치 안내를 따르십시오. 그런 다음 시스템 의존성을 설치하십시오:

xcode-select --install
brew install git python just llvm@22 bdw-gc pkg-config
export LLVM_SYS_221_PREFIX="$(brew --prefix llvm@22)"
export GC_LIB_DIR="$(brew --prefix bdw-gc)/lib"

빌드를 실행하기 전에 커맨드라인 도구 설치 프로그램이 끝날 때까지 기다리십시오. 이후의 터미널 세션에서도 두 export를 사용하려면 셸 설정에 넣어 두십시오. llvm@22는 의도적으로 지정한 것입니다. 버전을 지정하지 않은 LLVM 패키지는 우리의 Rust 바인딩이 사용하는 것과 다른 ABI를 선택할 수 있습니다.

Ubuntu

이 명령은 이 판을 쓸 당시의 최신 LTS인 Ubuntu 26.04 LTS를 대상으로 합니다. 지원 대상인 Ubuntu 24.04 CI 환경에서도 동작합니다. 공통 도구를 설치한 다음, 공식 LLVM 패키지 설치 프로그램을 사용해 버전 22를 명시적으로 선택하십시오:

sudo apt update
sudo apt install -y build-essential git curl wget ca-certificates \
  gnupg lsb-release software-properties-common python3 pkg-config \
  libgc-dev libssl-dev just
curl -fsSLo /tmp/wyah-llvm.sh https://apt.llvm.org/llvm.sh
sudo bash /tmp/wyah-llvm.sh 22
sudo apt install -y llvm-22-dev libpolly-22-dev
export LLVM_SYS_221_PREFIX=/usr/lib/llvm-22

버전이 붙은 LLVM 접두 경로는 이후 빌드를 위해 셸 설정에 넣어 두십시오. 배포판의 기본 Rust 패키지는 이 책이 고정한 컴파일러 버전과 다를 수 있으므로, 다음으로 rustup을 통해 Rust를 설치하십시오.

Rust와 저장소

공식 Rust 설치 안내서를 따르십시오. 두 플랫폼 모두에서 rustup 설치 명령은 다음과 같습니다:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"
git clone https://github.com/sdiehl/write-you-a-haskell.git
cd write-you-a-haskell
rustup show
rustc --version
cargo --version

이 체크아웃 안에서는 rust-toolchain.toml이 Clippy와 rustfmt를 포함한 Rust 1.99.0을 선택합니다. Rustup은 필요할 때 선택된 툴체인을 설치합니다. rustup 책에서 디렉터리 툴체인이 어떻게 동작하는지 설명합니다. just는 명령 실행기이며, 또 다른 빌드 시스템이 아닙니다. just 설치 안내서에는 위의 시스템 패키지 대신 쓸 수 있는 방법들이 나와 있습니다.

이제 작은 장 하나와 전체 워크스페이스를 빌드해 봅시다:

cargo run -p ch02_calc -- repl
just build
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings

HTML과 PDF를 만들려면 버전이 고정된 책 도구들을 로컬에 설치하십시오:

just bootstrap
just book
just pdf

부트스트랩은 mdBook과 그 전처리기들, Pandoc, Typst를 .tools 아래에 설치합니다. just 레시피는 .tools/bin을 PATH에 추가합니다. 글꼴은 이미 저장소에 포함되어 있습니다. book/build/html/index.html 또는 build/write-you-a-haskell.pdf를 여십시오. 공개된 사본은 https://sdiehl.github.io/write-you-a-haskell/의 HTML과 그 옆의 https://sdiehl.github.io/write-you-a-haskell/write-you-a-haskell.pdf에 있는 PDF이며, 이 PDF는 저장소의 최신 GitHub 릴리스에 첨부된 파일이기도 합니다. 전체 컴파일러 테스트는 cargo test --workspace로 실행하며, 깊은 재귀와 메모리 할당에 대한 회귀 테스트를 포함합니다.

Nix를 이용하는 방법

Nix를 설치했다면, flake가 Apple Silicon macOS와 두 Linux 아키텍처에서 쓸 수 있도록 고정된 Rust 툴체인, LLVM 22, Boehm, C 컴파일러, 개발 도구를 제공합니다:

nix --extra-experimental-features 'nix-command flakes' develop
just build
just bootstrap
just pdf

잠금 파일이 패키지 입력을 고정합니다. 이 셸은 LLVM과 GC 경로를 설정하므로, 위의 플랫폼별 패키지 설치 단계를 대신합니다. 그래도 부트스트랩은 여전히 책에 필요한 정확한 버전의 도구들을 .tools에 설치합니다.

왜 Rust인가

제가 2010년대 중반에 이 책을 처음 썼을 때, Haskell은 컴파일러를 설명하기에 매력적인 언어였습니다. 대수적 데이터 타입, 패턴 매칭, 간결한 구문 덕분에 변환 과정이 마치 그 수학적 정의처럼 읽혔습니다. 하지만 그런 정의에서 완전한 네이티브 컴파일러까지 나아가려면 당시에도 이미 지원이 빈약한 라이브러리에 기대야 했습니다. 제 경험상 그 유지보수 문제는 시간이 지날수록 더 나빠졌습니다.

저는 여전히 Haskell의 표면 언어를 좋아합니다. 하지만 이제는 이 책의 컴파일러를 만드는 데 Haskell이 실용적으로 좋은 선택이라고 생각하지 않습니다. 컴파일러에는 우아한 트리 변환뿐 아니라 꾸준히 유지보수되는 바인딩, 진단 기능, 테스트 도구, 패키징, 예측 가능한 런타임도 필요합니다. 암묵적인 지연 평가는 제가 작성하는 프로그램과 그 프로그램의 할당 및 평가 동작 사이에 거리를 더 벌려 놓기도 합니다. 컴파일러가 큰 입력을 안정적으로 처리해야 할 때 이러한 비용은 중요해집니다.

제가 이번 판에서 Rust를 선택한 이유는 훨씬 넓은 생태계와 프로덕션 도구에 대한 꾸준한 투자 때문입니다. Rust는 중간 언어를 위한 열거형과 패턴 매칭, 그 저장 공간을 위한 명시적 소유권, 그리고 LLVM과 네이티브 런타임에 대한 직접적인 접근을 제공합니다. Cargo 덕분에 각 장의 스냅숏을 함께 빌드하고 테스트하기가 쉽습니다. 저는 독자 여러분이 마지막 장을 마친 뒤에도 계속 쓸 수 있는 툴체인으로 컴파일을 배우는 데 시간을 쓰기를 바랍니다.

이 프로젝트에서 Rust는 표현력 있는 구현 코드와 실용적인 엔지니어링 사이에서 가장 좋은 균형을 이룹니다. 언젠가는 OCaml 판이 나올지도 모릅니다. 지금으로서는 Haskell은 여전히 공부할 가치가 있는 언어이고, Rust는 제가 무언가를 만들 때 쓰고 싶은 언어입니다. 원래의 Haskell 판은 저장소에 haskell-edition 태그로 보존되어 있습니다.

왜 지연 평가가 아닌가?

Haskell의 가장 강력한 교훈들은 모든 평범한 계산을 잠재적으로 지연되게 만들지 않아도 살아남습니다. 대수적 데이터, 패턴 매칭, 주요 타입(principal type), 순수성, 타입 클래스는 모두 유지할 가치가 있습니다. Fun은 이것들을 값에 의한 호출(call-by-value)과 함께 묶습니다. 인자는 호출 전에, 생성자 필드는 생성 전에, 평범한 바인딩은 그 본문 전에 계산됩니다. 이로써 독자는 작업이 언제 일어나고 저장 공간이 어디에 필요한지를 직접적으로 파악할 수 있습니다.

저는 암묵적 지연 평가가 새로운 언어에 생산적인 기본값이라고 보지 않습니다. 보류된 식은 그 원천 바인딩이 끝난 것처럼 보인 뒤에도 오랫동안 자신의 환경을 붙잡고 있을 수 있습니다. 그것을 강제하면 작업과 실패가 모두 그 원인에서 멀리 떨어진 곳으로 옮겨질 수 있습니다. 공유는 반복되는 작업을 피할 수 있지만, 그 자체의 할당 및 갱신 프로토콜을 도입합니다. 엄격성 분석은 많은 경우에 효율적인 실행을 되찾아 줄 수 있지만, 이는 처음부터 명시적으로 만들 수 있었던 비용 모델을 재구성하기 위한 상당한 장치입니다. 5장에서는 이 트레이드오프를 실행 가능한 형태로 살펴봅니다.

그래도 지연된 계산은 여전히 유용합니다. 이를 선택적으로 사용하게 만드는 데 제가 선호하는 기반은 call-by-push-value(CBPV)입니다. 이 방식은 값과 계산을 분리합니다. 계산은 값을 만들어 낼 수 있고, 값은 보류된 계산을 담을 수 있습니다. 명시적인 썽크(thunk) 연산과 강제(force) 연산이 그 경계를 표시합니다. 값에 의한 호출과 이름에 의한 호출(call-by-name)은 모두 이 더 작은 연산들을 통해 표현할 수 있습니다(Levy 2001). 따라서 프로그래머나 컴파일러는 지연을 모든 곳에서 물려받는 대신 지연이 들어가야 할 곳을 지정할 수 있습니다.

CBPV 자체는 중단된 계산을 메모이즈하지 않습니다. 필요에 의한 호출(call-by-need)에는 공유와 함께 결과를 기억하는 갱신이 추가로 필요합니다. 평범한 썽크를 두 번 강제하면 그 계산이 두 번 실행될 수 있습니다. 바로 그 차이 때문에 저는 지연 평가를 탐구할 때 이 토대를 선호합니다. Fun 자체는 여전히 엄격한 값에 의한 호출(call-by-value) 언어입니다. Fun은 CBPV도, 지연 값도 구현하지 않습니다. 지연을 명시적으로 다루는 일은 이 책의 컴파일러를 넘어서는, 이후 언어를 위한 방향입니다.

장별 로드맵

아래 표는 책 전체를 설명합니다. 링크된 제목을 누르면 각 장이 열립니다. 2–6장은 각각 독립된 언어입니다. 7–24장은 Fun의 연속된 스냅숏입니다. 각 행에는 선행 지식 목록 대신 여러분이 만들 부분이 적혀 있습니다.

1부: 기초

번호주제만들게 될 것
1소개워크스페이스, 테스트 러너, 장별 도구, HTML/PDF 책 파이프라인.
2파싱logos, LALRPOP, 비교용 Pratt 파서, 진단, REPL을 갖춘 산술 언어.
3람다 대수치환, 드 브루인 인덱스, 정규 순서 축약, 그리고 엄격한 클로저.
4단순 타입 람다 대수타입이 명시된 함수와 평가 전에 실행되는 타입 검사기.
5평가재귀 함수, 상수 스택 CEK 기계, 이름에 의한 호출(call-by-name) 비교.
6힌들리–밀너 추론알고리즘 W와 레벨 기반 유니온-파인드를 모두 사용한 다형적 let 추론입니다.

2부: 프런트엔드

번호주제만들게 될 것
7Fun의 설계소스 스팬, 진단, 패스 세션, Core, CLI를 갖춘 첫 번째 Fun 컴파일러입니다.
8레이아웃과 확장된 파서오프사이드 레이아웃, 주석을 보존하는 포매팅, 시그니처, 결합성 선언(fixity), 섹션입니다.
9리네이머고유 이름, 렉시컬 스코프, 의존성 컴포넌트, 상호 재귀입니다.
10타입이 있는 Core와 Core 린트System F 정교화(elaboration)와 컴파일러의 실수를 잡아내는 독립적인 Core 린트입니다.
11데이터 타입과 패턴 매칭대수적 데이터, 리스트와 튜플, 중첩 패턴, 등식, 가드입니다.
12레코드생성, 갱신, 패턴, 해석된 점 프로젝션을 갖춘 명목적 레코드입니다.
13카인드와 타입 생성자고차 카인드 생성자, 추론된 카인드, 검사되는 타입 동의어입니다.
14타입 클래스제약, 인스턴스, 슈퍼클래스, 기본값, 사전 전달, Double입니다.
15모나드, do 표기법, IO고차 카인드 제어 클래스, do 표기법, 불투명한 IO, 효과가 있는 프로그램.
16패턴 매칭 컴파일결정 트리와 조인 포인트, 그리고 완전성 및 중복 경고.
17Prelude와 파생소스로 작성한 전체 Prelude와 자동 생성되는 Eq, Ord, Show 인스턴스.

3부: 백엔드

번호주제만들게 될 것
18A-정규형과 조인 포인트타입 소거, ANF, 작은 단순화기, 반복적으로 실행하는 오라클.
19클로저 변환명시적 환경, 끌어올려진 함수, 부분 적용, 그리고 Flat IR 오라클.
20데이터 표현과 런타임태그가 붙은 워드, 힙 레이아웃, eval/apply, 공유 프리미티브, 그리고 Boehm 기반 런타임.
21LLVM 코드 생성네이티브 오브젝트 생성, 링킹, 꼬리 호출, 인터프리터와 대조하는 테스트.
22JIT와 REPLLLVM 실행과, 타입·카인드·컴파일러 패스를 살펴볼 수 있는 상태 유지형 REPL.
23최적화작업 보존, 도달 가능한 전역, 네이티브 수치 중간값.
24정밀 GC(선택)활성 루트 추적, 섀도 스택, 그리고 스트레스 테스트를 거친 복사 컬렉터를 다룹니다.
25향후 과제지연 평가, 모듈, 더 풍부한 타입, 최적화, 디버깅 확장을 위한 안내서입니다.

부록에는 문법, 타입 규칙, 중간 표현, 런타임 ABI, 진단 색인을 모았습니다. 책의 마지막은 참고 문헌으로 마무리합니다.

우리가 만들어 갈 방식

우리는 계산기로 시작합니다. 계산기는 충분히 작아서, 우리가 입력한 텍스트에서부터 그 식이 만들어 내는 답에 이르기까지 식 하나를 끝까지 따라가 볼 수 있습니다. 그다음에는 함수, 평가, 타입을 각각 따로 탐구하기 위해 작은 언어 몇 개를 만듭니다. 각 언어에는 직접 실행하고 수정해 볼 수 있는 완전한 Rust 구현이 있습니다. 컴파일러의 한 부분을 실험해 보기 위해 컴파일러 전체를 먼저 이해할 필요는 없습니다.

7장에서 이 아이디어들이 Fun으로 합쳐집니다. 그 뒤로 각 장은 이전 장의 컴파일러에서 출발해 한 조각씩 더해 갑니다. 레코드는 언어를 확장하고, 타입 클래스는 여러 타입에 걸쳐 연산을 공유하는 방법을 도입하며, 이후의 장들에서는 컴파일러가 네이티브 코드를 생성하도록 가르칩니다. 모든 장에는 저마다의 크레이트가 있으므로, 지금 공부하는 버전 옆에 이전 버전을 열어 두고 함께 볼 수 있습니다.

완성된 코드뿐 아니라 변경 사항도 읽어 보십시오. 각 장은 새로 추가되거나 변경된 소스 파일을 중요한 것부터 나열합니다. 두 스냅숏을 비교해 보면 어떤 기능이 실제로 컴파일러에 무엇을 요구하는지 알 수 있습니다. 새로운 구문 노드, 타입 규칙, 그리고 더 단순한 연산으로의 변환이 필요할 수도 있습니다. 이 책은 그 소스 파일들에서 코드를 가져와 싣기 때문에, 여러분이 읽는 구현이 곧 여러분이 실행하는 구현입니다. 테스트도 다음 장으로 이어지므로, 기능을 추가해도 이미 동작하던 부분이 망가지지 않았는지 확인하는 데 도움이 됩니다.

이론은 그것이 해결을 돕는 문제와 함께 등장합니다. 우리는 작은 예제를 시도해 보고, 그 예제가 제기하는 질문을 찾은 다음, 답을 정확하게 표현할 수 있을 만큼의 표기법을 도입할 것입니다. 코드와 예제를 먼저 따라가 본 뒤, 규칙이 실제로 작동하는 모습을 보고 나서 다시 그 규칙으로 돌아와도 됩니다. 타입 이론 강의를 미리 들었다고 가정하지 않습니다. Rust의 열거형, 패턴 매칭, 빌림, Cargo에 익숙하다면 구현을 따라가는 데 도움이 될 것입니다.

이론

컴파일러는 컴퓨터가 프로그램을 실행할 수 있게 되기까지 프로그램을 여러 번 다시 씁니다. 핵심 과제는 프로그램이 하는 일은 그대로 유지하면서 프로그램이 쓰인 방식을 바꾸는 것입니다. 여러분의 프로그램이 42를 출력한다면, 우리가 생성하는 기계어 코드도 42를 출력해야 합니다. 프로그램이 인사하기 전에 이름을 묻는다면, 컴파일은 그 동작들의 순서를 그대로 지켜야 합니다.

우리는 이 단계들을 하나씩 쌓아 올릴 것입니다. 먼저 소스 텍스트가 어떻게 짜여 있는지 알아냅니다. 1 + 2 * 3에서 곱셈은 덧셈 안에 속합니다. 그다음에는 연산들이 말이 되는지 검사하므로, 숫자를 함수에 더하는 일은 프로그램이 실행되기 전에 잡힙니다. 이후의 장에서는 프로그램을 기계어 코드로 바꾸기 더 쉬운 단순한 형태로 변환합니다.

변환을 제대로 했는지 어떻게 알 수 있을까요? 한 가지 유용한 요령은 각 형태마다 작은 인터프리터를 작성하고 같은 예제들을 그 인터프리터들에 돌려 보는 것입니다. 서로 다른 답이 나온다면 컴파일러 버그를 찾은 것입니다. 답이 일치한다고 해서 가능한 모든 프로그램이 동작한다는 것이 입증되지는 않지만, 진행하면서 작업을 확인할 수 있는 실용적인 방법이 됩니다. 그 과정에서 우리는 타입과 평가 규칙이 테스트할 수 있는 예제를 넘어 프로그램에 대해 추론하는 데 어떻게 도움이 되는지도 배울 것입니다 (Pierce 2002).

구현

저장소는 이 책의 일부입니다. 각 장에는 실행하고, 뜯어 보고, 고쳐 볼 수 있는 크레이트가 있습니다. 어떤 변환이 실제로 무엇을 했는지 궁금하다면, 컴파일러에게 출력을 덤프하게 한 뒤 입력과 비교해 보십시오. 그런 습관이 구현을 뚫어지게 바라보며 깨달음이 오기를 바라는 것보다 더 많은 것을 알려 줄 것입니다.

우리는 또한 컴파일러가 커지는 동안에도 예제들이 계속 동작하기를 원합니다. testkit은 작은 프로그램들을 실행하고 그 출력, 오류, 종료 상태를 검사합니다. 나중에는 같은 프로그램을 여러 실행 엔진으로 실행합니다. 두 엔진의 결과가 서로 다르다면 조사할 구체적인 대상이 생긴 것입니다.

#![allow(unused)]
fn main() {
/// The chapter supplies its binary and available engines; the runner owns policy.
pub struct Config<'a> {
    pub binary: &'a Path,
    pub programs: &'a Path,
    pub engines: &'a [Engine],
    pub timeout: Duration,
}
}

xtask는 장을 복사하고 물려받은 예제들을 검사하는 반복 작업을 처리합니다. 책 자체는 각 장의 소스에서 Rust 코드를 직접 포함합니다. 포함된 함수를 따라 크레이트 안으로 들어가 지금 읽고 있는 코드로 실험해 볼 수 있습니다. HTML과 PDF는 같은 장들로부터 빌드됩니다.

실행해 보기

just bootstrap으로 버전이 고정된 책 도구들을 설치한 다음, 다음을 실행하십시오:

$ cargo test --workspace
$ just book
$ just pdf

HTML은 book/build/html/에 있습니다. PDF는 build/write-you-a-haskell.pdf입니다. 컴파일러의 --dump 명령은 2장부터 시작합니다. 1장에는 덤프할 컴파일러 패스가 없습니다.

더 읽을거리

Haskell Report는 이 언어의 레퍼런스입니다 (Simon Marlow 2010). Pierce는 우리가 사용할 연산적 판단과 타입 판단을 소개합니다 (Pierce 2002). Kiselyov는 타입 일반화를 의존성 추적과 연결하며, 이는 6장을 위한 준비가 됩니다 (Kiselyov 2013). 우리의 선택을 실제 구현과 비교해 보고 싶다면, Takenobu Tani의 GHC Reading Guide(pdf / github)가 GHC 소스 트리와 그 파이프라인을 안내하는 지도 역할을 합니다.

2장: 파싱

새 파일

  • src/lexer.rs
  • src/pratt.rs
  • src/ast.rs
  • src/eval.rs
  • build.rs
  • src/diagnostic.rs
  • src/lib.rs
  • src/main.rs

변경된 파일

없음.

동기

우리는 1 + 2 * 3을 읽고 곱셈이 먼저라는 것을 압니다. 컴퓨터가 받는 것은 문자열입니다. 컴퓨터가 무엇이든 계산하려면 먼저 그 문자들이 어떻게 맞물리는지 가르쳐야 합니다. 그것이 파서의 일입니다.

우리는 소스 텍스트에서 답에 이르기까지 전 과정을 따라갈 수 있을 만큼 작은 계산기부터 시작하겠습니다. 이 계산기의 파서는 두 번 만들 것입니다. LALRPOP을 쓰면 문법을 적기만 해도 파싱 장치를 대신 생성해 줍니다. Pratt 파서는 연산자를 처리하는 로직을 짧은 Rust 함수 하나에 담습니다. 둘을 모두 살펴보면 파서 생성기가 우리 대신 무엇을 하고 있는지 알아내는 데 도움이 됩니다.

현재의 언어

프로그램은 식 하나입니다. 이 언어에는 십진 정수 리터럴, 이름, +, -, *, /, 단항 마이너스, 괄호, 그리고 비재귀 let이 있습니다.

let x = 7 in (let y = x + 1 in y * y) - x

바인딩은 본문에서 보이며, 우변에서는 보이지 않습니다. 중첩된 바인딩은 바깥쪽 이름을 가립니다. 곱셈과 나눗셈은 덧셈과 뺄셈보다 강하게 결합합니다. 네 연산자는 모두 왼쪽으로 결합합니다. 단항 마이너스는 이들보다 더 강하게 결합합니다. 이 미니 언어에서 / 연산자는 정수 나눗셈 결과를 0 쪽으로 버림합니다. 완전한 Fun에서는 /를 분수 산술용으로 남겨 두고, 이 연산은 quot로 표기할 것입니다.

이론

1 + 2 * 3에 대해서는 그럴듯한 트리가 두 가지 있습니다. 하나는 덧셈을 먼저 하여 9를 반환합니다. 다른 하나는 곱셈을 먼저 하여 7을 반환합니다. 우리가 원하는 것은 두 번째이므로, 문법이 그 선택을 표현할 수 있어야 합니다. 우리는 합, 곱, 아톰을 서로 다른 규칙으로 분리하겠습니다. 합은 곱 전체를 하나의 피연산자로 포함할 수 있습니다. 괄호를 쓰면 다른 묶음을 원할 때 전체 식 문법에서 다시 시작할 수 있습니다.

파싱 전략 고르기

모든 면에서 앞서는 단 하나의 파싱 기법은 없습니다. 어떤 접근법은 문법 전체를 살펴보기 쉽게 해 줍니다. 또 어떤 접근법은 호스트 언어로 작은 파서를 직접 작성하기 쉽게 해 줍니다. 고르기 전에, 흔히 한데 묶어 생각하는 세 가지를 구분해 두면 도움이 됩니다. 문법은 어떤 구문을 받아들일지 정하고, 알고리즘은 그것을 인식하며, API는 우리가 파서를 어떻게 작성할지를 결정합니다.

접근법핵심 아이디어이 책에서의 트레이드오프
LR(1) / LALR(1)생성된 상태 기계가 토큰을 시프트하고 문법 생성 규칙을 리듀스합니다.언어 전체를 선언적으로 기술하는 문법이며, 충돌은 생성 과정에서 보고됩니다.
Pratt식 파서가 결합력(binding power)을 사용해 다음 피연산자가 어느 연산자에 속하는지 결정합니다.작고 읽기 쉬운 연산자 파싱입니다. 선언과 블록에는 여전히 별도의 문법이 필요합니다.
Packrat파싱 결과를 규칙과 입력 위치별로 메모이제이션합니다.적합한 고정 문법에 대해 인식 시간을 예측할 수 있지만, 그 대가로 메모 테이블이 필요합니다.
파서 콤비네이터평범한 함수를 사용해 작은 파서들을 조합합니다.편리한 추상화입니다. 백트래킹, 메모이제이션, 오류 처리 방식은 라이브러리에 따라 달라집니다.

LR(1)은 왼쪽에서 오른쪽으로 읽어 나가며 최우단 유도(rightmost derivation)를 역순으로 구성합니다. LR(1)의 상태는 이미 읽은 접두부를 요약합니다. 미리 보기(lookahead) 토큰 하나가 다음 토큰을 시프트할지, 완성된 생성 규칙을 리듀스할지를 결정하는 데 도움을 줍니다. “1”이라는 숫자가 식을 토큰 하나로 제한한다는 뜻은 아닙니다. 파서의 스택은 임의로 깊은 구문 문맥을 유지합니다. 더 작은 합을 포함하는 합과 같은 좌재귀는 이러한 상향식 과정에 자연스럽게 들어맞습니다.

LALR(1)은 기반이 되는 LR(0) 항목이 같은 LR(1) 상태들을 병합하고, 그 미리 보기 집합들을 합칩니다. 이렇게 하면 테이블 크기가 줄어들지만, 병합하지 않은 문법에는 없던 충돌이 생길 수 있습니다. 이름과 달리 우리가 사용하는 생성기는 기본적으로 LR(1)을 사용합니다. LALR(1)은 선택 사항입니다. LALRPOP 문서를 참고하십시오. 충돌은 유용한 피드백입니다. 생성된 파서를 신뢰하려면 먼저 문법을 손봐야 합니다.

Pratt 파싱은 연산자에 관한 결정을 국소적으로 내립니다. 1 + 2를 읽은 뒤 파서는 뒤따르는 *를 +의 결합력(binding power)과 비교합니다. 곱셈은 오른쪽 피연산자 안에 속하므로 1 + 2 * 3은 1 + (2 * 3)이 됩니다. 8 - 3 - 1의 경우 오른쪽 피연산자의 문턱값이 같은 세기의 뺄셈을 배제하므로 (8 - 3) - 1이 됩니다. -의 전위 용법과 중위 용법에는 서로 다른 처리기를 둘 수 있습니다. 이 방식은 식에는 아주 잘 맞지만, 그 자체로는 클래스 선언이나 레이아웃 블록의 구조를 규정하지 않습니다.

Packrat 파싱은 각 위치에서 규칙의 성공 또는 실패를 기억해 두어 백트래킹 중의 중복 작업을 피합니다. 이 기법은 흔히 파싱 식 문법(PEG)과 함께 거론되며, 이 문법의 대안들은 순서 있는 선택(ordered choice)을 사용합니다. 처음으로 성공한 대안이 채택됩니다. 따라서 대안의 순서를 바꾸면 언어가 달라질 수 있습니다. 메모이제이션된 항목마다 작업량이 제한된 고정 문법이라면, 이 방식은 선형 인식 시간을 보장하며 메모 저장 공간도 대체로 선형입니다. 일반적인 좌재귀 규칙은 다시 작성하거나 특수한 확장을 사용해야 합니다. 이러한 선택들은 Ford의 packrat과 PEG 개요에서 자세히 다룹니다.

파서 콤비네이터는 파서를 작성하는 방법이지, 하나의 인식 알고리즘이 아닙니다. 순차 콤비네이터는 한 파서를 실행한 뒤 다른 파서를 실행합니다. 선택 콤비네이터는 해당 라이브러리의 규칙에 따라 대안들을 시도합니다. 콤비네이터 라이브러리는 커밋하거나, 백트래킹하거나, 메모이제이션할 수 있습니다. 겉보기에 편리하다고 해서 선형 시간, 좋은 오류 복구, 좌재귀 지원이 저절로 주어지지는 않습니다. 이것들은 여전히 설계상의 선택으로 남습니다.

왜 LALRPOP와 비교용 Pratt 파서를 함께 쓸까요?

LALRPOP는 언어 전체에 대해 들여다볼 수 있는 하나의 문법, AST를 구성하기 위한 타입이 있는 Rust 액션, 그리고 명시적인 복구 생성 규칙을 제공합니다. Pratt는 계산기의 우선순위 규칙을 간결하게 설명하는 두 번째 방식을 제공합니다. 여기서 둘 다 구현하면 묶음(grouping) 결정이 구체적으로 드러나고, 독립적인 비교 대상도 얻을 수 있습니다. 이후의 컴파일러는 LALRPOP를 유지합니다. 두 파서를 모두 실행하지는 않습니다.

Haskell 스타일 구문에는 문법 바깥에서 해야 할 작업도 있습니다. 8장에서 logos는 토큰을 인식하고, marginalia는 포매팅을 위해 주석과 공백을 보존하며, offsides는 들여쓰기를 블록 구분자로 변환합니다. 뒤의 두 라이브러리는 이 프런트엔드에 필요한 트리비아(trivia)와 Haskell 스타일 레이아웃 지원을 제공합니다. 그런 다음 LALRPOP가 이렇게 준비된 토큰 스트림을 소비합니다. 레이아웃은 파서 액션 곳곳에 흩어지는 대신, 별도로 테스트할 수 있는 독립된 변환으로 드러나 있습니다. 이 계산기에는 아직 레이아웃도 주석 보존도 없습니다.

사용자가 선언하는 결합성(fixity)은 별개의 문제를 일으킵니다. 생성된 파서는 소스 파일마다 다른 우선순위 표를 가질 수 없습니다. 8장에서는 평평한 연산자 연쇄를 파싱한 뒤 그 묶음을 나중에 해석합니다. Pratt 루프든 파서 생성기든, 그러한 선언이 언제 알려지는지를 결정해야 할 필요성을 없애 주지는 않습니다.

구문과 타입

이 작은 언어는 이미 타입 판단으로 기술할 수 있습니다. 두 피연산자의 타입이 Int이면, 그 합의 타입도 Int입니다:

값이 정수뿐이므로 아직은 타입 추론기가 필요하지 않습니다. 다만 평가하기 전에 렉시컬 스코프는 검사합니다. Pierce는 구문, 타입, 평가 사이의 구분을 자세히 전개합니다 (Pierce 2002). Haskell Report도 비교 자료로 유용하지만, Fun의 이후 결합성 규칙은 여기서 의도적으로 빠져 있습니다 (Simon Marlow 2010).

구현

lexer.rs는 logos를 사용해 토큰을 인식하고 바이트 범위(span)를 보존합니다. 길이가 같으면 키워드가 식별자보다 우선합니다. 잘못된 문자나 범위를 벗어난 리터럴은 E0001을 발생시킵니다. ast.rs는 각 식에 범위를 저장하므로, 소스 정보를 잃지 않고 두 파서를 비교할 수 있습니다.

#![allow(unused)]
fn main() {
/// Byte offsets into the single source file, including parentheses when present.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Expr {
    pub kind: Kind,
    pub span: Range<usize>,
}

/// An error node exists only during recovery; the driver never evaluates it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Kind {
    Int(i64),
    Var(String),
    Neg(Box<Expr>),
    Binary(Op, Box<Expr>, Box<Expr>),
    Let(String, Box<Expr>, Box<Expr>),
    Error,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
    Add,
    Sub,
    Mul,
    Div,
}
}

생성된 파서는 비단말 기호로 우선순위를 구현합니다. 전체 문법은 바인딩, 괄호, 단항 마이너스, 그리고 복구 생성 규칙도 처리합니다.

Sum: Expr = {
    <a:Sum> "+" <b:Product> => Expr::binary(Op::Add, a, b),
    <a:Sum> "-" <b:Product> => Expr::binary(Op::Sub, a, b),
    Product,
};
Product: Expr = {
    <a:Product> "*" <b:Unary> => Expr::binary(Op::Mul, a, b),
    <a:Product> "/" <b:Unary> => Expr::binary(Op::Div, a, b),
    Unary,
};

! 생성 규칙은 오류 노드를 삽입하고 복구 진단을 저장합니다. 드라이버는 복구로 트리가 만들어지더라도 오류를 반환합니다. 평가기는 복구 노드를 절대 받지 않습니다. diagnostic.rs는 안정적인 오류 코드 줄과 위치를 출력한 다음, ariadne가 바이트 인덱싱으로 소스 발췌를 렌더링하게 합니다.

#![allow(unused)]
fn main() {
/// Parse and recover enough to report several syntax errors, then stop the pass.
pub fn parse(source: &str) -> Result<Expr, Vec<Diagnostic>> {
    let tokens = lexer::lex(source)?;
    let mut recovered = Vec::new();
    let parsed = parser::ExprParser::new()
        .parse(&mut recovered, tokens.into_iter().map(Ok));
    let mut errors: Vec<_> = recovered
        .into_iter()
        .map(|r| parse_error(r.error))
        .collect();
    match parsed {
        Ok(expr) if errors.is_empty() => Ok(expr),
        Ok(_) => Err(errors),
        Err(error) => {
            errors.push(parse_error(error));
            Err(errors)
        }
    }
}
}

Pratt 파서의 루프는 결합력이 충분히 높은 연산자를 소비합니다. 오른쪽 피연산자를 power + 1에서 파싱하면 연산자가 왼쪽 결합이 됩니다. 전위 파싱은 이름, 리터럴, 괄호, 부정을 처리합니다. let은 별도의 진입점이 처리합니다.

#![allow(unused)]
fn main() {
fn infix(&mut self, min_power: u8) -> Result<Expr, Diagnostic> {
    let mut left = self.prefix()?;
    loop {
        let (op, power) = match self.peek() {
            Some(Token::Plus) => (Op::Add, 1),
            Some(Token::Minus) => (Op::Sub, 1),
            Some(Token::Star) => (Op::Mul, 3),
            Some(Token::Slash) => (Op::Div, 3),
            _ => break,
        };
        if power < min_power {
            break;
        }
        self.at += 1;
        let right = self.infix(power + 1)?;
        left = Expr::binary(op, left, right);
    }
    Ok(left)
}
}

eval.rs는 끝에서부터 탐색하는 바인딩 스택을 사용해 렉시컬 스코프를 모델링합니다. 이항 연산자의 왼쪽 피연산자를 오른쪽 피연산자보다 먼저 평가하고, 바인딩 본문이 실패하더라도 환경을 복원합니다. 산술은 처음부터 랩어라운드되는 63비트 값을 사용하므로 Rust의 디버그 오버플로 검사에 의존하지 않습니다. 정규화 함수는 Rust 항목으로 포함됩니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::{Expr, Kind, Op},
    diagnostic::Diagnostic,
};
/// Check every variable occurrence without executing arithmetic.
pub fn check(expr: &Expr) -> Result<(), Vec<Diagnostic>> {
    fn visit(
        expr: &Expr,
        names: &mut Vec<String>,
        errors: &mut Vec<Diagnostic>,
    ) {
        match &expr.kind {
            Kind::Var(name) if !names.contains(name) => {
                errors.push(Diagnostic::new(
                    "E0101",
                    format!("unbound variable `{name}`"),
                    expr.span.clone(),
                ))
            }
            Kind::Neg(a) => visit(a, names, errors),
            Kind::Binary(_, a, b) => {
                visit(a, names, errors);
                visit(b, names, errors);
            }
            Kind::Let(name, a, b) => {
                visit(a, names, errors);
                names.push(name.clone());
                visit(b, names, errors);
                names.pop();
            }
            _ => {}
        }
    }
    let mut errors = Vec::new();
    visit(expr, &mut Vec::new(), &mut errors);
    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors)
    }
}
/// The environment is a stack: the most recent binding shadows earlier names.
pub fn evaluate(
    expr: &Expr,
    env: &mut Vec<(String, i64)>,
) -> Result<i64, String> {
    match &expr.kind {
        Kind::Int(n) => Ok(normalize(*n)),
        Kind::Var(name) => env
            .iter()
            .rev()
            .find(|(key, _)| key == name)
            .map(|(_, value)| *value)
            .ok_or_else(|| format!("unbound variable `{name}`")),
        Kind::Neg(value) => {
            Ok(normalize(evaluate(value, env)?.wrapping_neg()))
        }
        Kind::Binary(op, left, right) => {
            let a = evaluate(left, env)?;
            let b = evaluate(right, env)?;
            Ok(normalize(match op {
                Op::Add => a.wrapping_add(b),
                Op::Sub => a.wrapping_sub(b),
                Op::Mul => a.wrapping_mul(b),
                Op::Div if b == 0 => return Err("divide by zero".into()),
                Op::Div => a / b,
            }))
        }
        Kind::Let(name, value, body) => {
            let value = evaluate(value, env)?;
            env.push((name.clone(), value));
            let result = evaluate(body, env);
            env.pop();
            result
        }
        Kind::Error => {
            Err("internal error: attempted to evaluate a recovery node"
                .into())
        }
    }
}
pub fn normalize(n: i64) -> i64 {
    (n << 1) >> 1
}
}
#![allow(unused)]
fn main() {
/// The environment is a stack: the most recent binding shadows earlier names.
pub fn evaluate(
    expr: &Expr,
    env: &mut Vec<(String, i64)>,
) -> Result<i64, String> {
    match &expr.kind {
        Kind::Int(n) => Ok(normalize(*n)),
        Kind::Var(name) => env
            .iter()
            .rev()
            .find(|(key, _)| key == name)
            .map(|(_, value)| *value)
            .ok_or_else(|| format!("unbound variable `{name}`")),
        Kind::Neg(value) => {
            Ok(normalize(evaluate(value, env)?.wrapping_neg()))
        }
        Kind::Binary(op, left, right) => {
            let a = evaluate(left, env)?;
            let b = evaluate(right, env)?;
            Ok(normalize(match op {
                Op::Add => a.wrapping_add(b),
                Op::Sub => a.wrapping_sub(b),
                Op::Mul => a.wrapping_mul(b),
                Op::Div if b == 0 => return Err("divide by zero".into()),
                Op::Div => a / b,
            }))
        }
        Kind::Let(name, value, body) => {
            let value = evaluate(value, env)?;
            env.push((name.clone(), value));
            let result = evaluate(body, env);
            env.pop();
            result
        }
        Kind::Error => {
            Err("internal error: attempted to evaluate a recovery node"
                .into())
        }
    }
}
}

main.rs는 이 조각들을 명령과 REPL에 연결합니다. 같은 식에 두 파서를 모두 적용해 보고 각각의 트리를 살펴보십시오. 테스트도 이 작업을 하며, 서로 다른 묶음을 가진 생성된 식들도 포함합니다. 트리를 비교하면 우연히 맞아떨어진 수치 결과가 가릴 수 있는 실수를 잡아낼 수 있습니다.

실행해 보기

$ cd crates/ch02_calc
$ cargo run -q -- run tests/programs/precedence.fun
7
$ cargo run -q -- dump --pass=parse tests/programs/precedence.fun
(+ 1 (* 2 3))
$ cargo run -q -- run tests/programs/precedence.fun --parser=pratt
7
$ cargo run -q -- repl
Calc: :quit to exit, :parse EXPR to inspect the AST.
calc> let x = 6 in x * 7
42
calc> :quit

check는 산술을 실행하지 않고 구문과 이름을 검증합니다. 따라서 1 / 0은 check를 통과하지만, run은 fun: divide by zero를 보고하고 상태 1로 종료합니다. 렉서 스팬을 살펴보려면 dump --pass=tokens를 사용하십시오.

연습 문제

  1. 쉬움. 20 - 3 * 4 - 1의 값과 파스 덤프를 모두 예측한 다음 실행해 보십시오.
  2. 쉬움. 1과 (1)의 스팬을 비교하십시오. 두 값이 같은 이유를 설명하십시오.
  3. 보통. Pratt 파서의 오른쪽 결합력을 power로 바꾸십시오. 결합성 변화를 드러내는 가장 작은 뺄셈 식을 찾으십시오.
  4. 보통. 서로 독립적인 구문 오류 두 개를 보고하는 잘못된 식을 작성하십시오. 복구가 어디에서 재개될 수 있는지 설명하십시오.
  5. 어려움. 생성기를 확장해 불필요한 괄호를 삽입하게 하고, 평가 결과가 바뀌지 않음을 검증하십시오.

더 읽을거리

Pierce는 구문과 의미를 신중하게 구분합니다 (Pierce 2002). Haskell Report는 훨씬 더 큰 구체 구문과 결합성 해석 절차를 규정합니다(Simon Marlow 2010).

3장: 람다 대수

새 파일

  • src/names.rs
  • src/normal.rs

변경된 파일

  • src/eval.rs
  • src/ast.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs

동기

우리 계산기는 수에 이름을 붙일 수 있습니다. 계산에 이름을 붙이고 그것을 여러 입력에 사용할 수 있다면 어떨까요? 그것이 바로 함수이며, 함수를 추가하면 놀라울 만큼 많은 것을 할 수 있게 됩니다. 람다와 적용만으로 순수 비타입 람다 대수를 표현할 수 있습니다. 처치 인코딩(Church encoding)에서 보겠지만, 데이터조차 함수로 표현할 수 있습니다 (Pierce 2002). 구문 두 가지만으로 이만큼의 표현력을 얻는다니 놀랍습니다!

제대로 짚고 넘어가야 할 세부 사항이 몇 가지 있습니다. 두 변수의 이름이 모두 x라면, 함수가 가리키는 것이 어느 쪽인지 어떻게 알 수 있겠습니까? 또 함수를 호출할 때 인자를 먼저 평가해야 하겠습니까? Rust의 답을 슬쩍 빌려 쓰는 대신, 치환과 클로저를 직접 만들어 그 답을 눈으로 확인하겠습니다.

현재의 언어

프로그램은 여전히 단일 식입니다. 람다는 매개변수를 하나 받고, 적용은 왼쪽 결합이며, 불리언이 if 식을 제어합니다. 비교는 정수 연산입니다. 아직 타입 표기는 없습니다.

let twice = \f -> \x -> f (f x) in twice (\x -> x + 1) 0

클로저는 자신의 람다가 평가된 곳에 있던 바인딩을 기억합니다. let x = 1 in let f = \y -> x + y in let x = 100 in f 2의 결과는 3입니다. 대신 호출한 쪽의 x 바인딩을 사용한다면 동적 스코프가 됩니다.

이론

비타입 문법

순수한 타입 없는 람다 대수에는 형태가 단 세 가지뿐입니다. 식은 변수, 적용, 추상 중 하나입니다. 각 세로 막대는 “또는“으로 읽으십시오.

이것이 대수의 전부입니다! 우리의 작은 언어는 모든 수와 불리언을 함수로 인코딩하지 않고도 실험할 수 있도록 몇 가지 편의 기능을 더합니다.

여기서 은 정수이고 는 +, -, *, /, ==, <, <= 중 하나입니다. 괄호는 트리 노드를 추가하지 않고 식을 묶습니다. 파서에는 오류 복구를 위한 Error 자리 표시자도 있지만, 이는 받아들여지는 프로그램의 형태가 아닙니다. “타입 없는”이라는 말은 아직 정적 타입 검사기가 없다는 뜻이지, 이 형태들을 어떻게 조합하든 런타임에 의미가 있다는 뜻이 아닙니다.

축약과 평가

람다를 호출한다는 것은 그 매개변수를 주어진 인자로 치환한다는 뜻입니다. 이 단계를 베타 축약(beta reduction)이라고 부릅니다. \x -> x + 1을 41에 적용하면, x를 치환하여 41 + 1을 얻습니다. 여기까지는 문제가 없습니다. 미묘한 문제는 이름이 반복될 때 시작됩니다. 우리는 본문에서 매개변수의 자유 출현만 치환하며, 안쪽 바인더가 인자의 자유 이름을 포획하게 된다면 그 바인더의 이름을 바꿉니다. 바로 그 실수를 아래에서 직접 살펴보겠습니다 (Pierce 2002).

평가 순서는 다음에 어떤 축약을 수행할지 결정합니다. 정규 순서는 가장 왼쪽의 가장 바깥쪽 축약 가능한 식을 택하며, 우리의 완전 축약기는 람다 안쪽까지 계속 축약합니다. 값에 의한 호출은 먼저 함수와 그 인자를 평가한 다음 본문으로 들어갑니다. 람다 자체는 이미 값입니다.

그 차이는 함수가 영원히 실행될 인자를 무시할 때 드러납니다. 정규 순서는 그 인자를 건너뛸 수 있습니다. 값에 의한 호출은 그 인자를 계산하려다 막혀 버립니다. Y 콤비네이터는 특히 인상적인 예를 보여 줍니다. Y 조합자는 정규 순서에서는 팩토리얼을 계산하지만, 우리의 엄격한 평가기에서는 연료를 모두 소진합니다. 연료가 바닥났다는 것은 우리 실험의 한계에 도달했다는 뜻이지, 그 계산이 결코 끝나지 않음을 증명했다는 뜻이 아닙니다.

왜 드 브루인 인덱스인가?

이름은 프로그램을 읽는 데 도움이 되지만, 이름의 철자는 바인딩과는 무관합니다. \x -> x와 \answer -> answer는 같은 함수를 나타냅니다. 우리는 이 둘을 알파 동치(alpha-equivalent)라고 부릅니다. 묶인 변수의 이름을 일관되게 바꾸는 것은 아무것도 바꾸지 않습니다. 하지만 두 구문 트리를 직접 비교하면 서로 다른 문자열이 나옵니다. 이름 있는 항을 다시 쓰는 컴파일러는 무해한 이름 바꾸기와, 어떤 출현이 가리키는 바인더 자체가 바뀌는 변화를 계속 구별해야 합니다.

y가 자유 변수인 다음 열린 항의 축약을 살펴봅시다:

(\x -> \y -> x) y

x를 y로 기계적으로 바꾸면 \y -> y가 됩니다. 이는 틀렸습니다. 이 함수는 자기 매개변수와 관계없이, 인자로 주어진 자유 변수 y를 반환해야 합니다. 먼저 안쪽 매개변수의 이름을 바꾸십시오:

(\x -> \z -> x) y
→ \z -> y

이것이 우리의 치환 함수가 해결해야 하는 포획(capture) 문제입니다. 드 브루인 인덱스(De Bruijn index)는 새 이름의 철자에 의존하지 않는 바인딩 표현을 제공합니다. 각 묶인 출현을 0부터 세어 그 바인더까지의 거리로 바꾸십시오. 적용 노드나 소스 줄이 아니라, 감싸고 있는 바인더를 세십시오.

아래 예제에서는 이름 없는 람다를 λ. body로 씁니다. 여기 나오는 숫자는 정수 리터럴이 아니라 변수 인덱스입니다. 이는 설명을 위한 표기법이지, 이 장의 파서가 받는 입력 구문이 아닙니다.

이름 있는 항인덱스 항읽는 법
\x -> xλ. 0가장 가까운 매개변수를 반환합니다.
\x -> \y -> xλ. λ. 1y를 건너뛰어 x에 도달합니다.
\x -> \y -> yλ. λ. 0y를 반환합니다.
\f -> \x -> f xλ. λ. 1 0바깥 매개변수를 안쪽 매개변수에 적용합니다.
\x -> \x -> xλ. λ. 0안쪽 x가 바깥쪽 x를 가립니다.

더 긴 예로 \f -> \x -> f (f x) 안으로 들어가 봅시다. 첫 번째 람다에 들어가면 f를 스코프 스택에 넣습니다. 두 번째 람다에 들어가면 x를 넣습니다:

scope, nearest first:  [x, f]
index:                  0  1

f (f x)                 1 (1 0)
\f -> \x -> f (f x)     λ. λ. 1 (1 0)

f가 나타나는 두 곳 모두 인덱스가 1입니다. 적용은 바인더를 추가하지 않으므로, (f x) 안으로 들어가도 어느 인덱스도 바뀌지 않습니다. 매개변수 이름을 g와 seed로 바꾸어도 인덱스가 매겨진 트리는 정확히 똑같습니다. 알파 동치가 평범한 구조적 동등성이 된 것입니다 (Pierce 2002). 이 점은 우리의 프린터 테스트에서 유용합니다. 파싱–출력–파싱 왕복은 이름이 바뀌더라도 바인딩을 보존해야 합니다.

열린 항에는 여전히 자유 변수를 표현할 방법이 필요합니다. 우리의 Term 타입은 자유 변수를 Bound(index)와 구별하여 Free(name)으로 보관합니다. 따라서 \x -> x y는 Lam(App(Bound(0), Free("y")))가 됩니다. 재귀적이지 않은 let은 본문에서만 바인더를 도입합니다. let x = x in x에서 오른쪽의 x는 자유 변수이고 마지막 x는 바인딩된 변수입니다:

Let(Free("x"), Bound(0))

인덱스를 쓰면 이름 바꾸기는 간단해지지만, 항을 바인더 너머로 옮길 때는 여전히 주의가 필요합니다. \a -> (\x -> \b -> x) a 안에서 인자 a는 원래 위치에서 인덱스 0을 가집니다. 축약 후의 결과는 \a -> \b -> a이며, 여기서는 같은 출현이 인덱스 1이 되어야 합니다. 예전의 0을 그대로 복사하면 실수로 b를 가리키게 됩니다. 인덱스가 매겨진 항에 직접 치환하는 평가기에는 시프팅(shifting)이 필요합니다: 항이 스코프 안으로 들어가거나 밖으로 나올 때 바깥 바인더를 가리키는 참조는 조정하고, 항 내부에서 바인딩된 참조는 그대로 두는 것입니다.

여기서는 인덱스를 변환과 비교에 사용합니다. 정규 순서 축약기는 여전히 이름이 있는 트리에서 치환하고, 엄격한 평가기는 클로저를 사용합니다. 그렇게 하면 각 예제가 핵심에 집중할 수 있습니다. 이후의 Fun 패스들은 덤프를 읽기 쉽게 유지하기 위해 고유한 이름을 사용합니다. 이름 없는 항은 바인딩을 이해하는 데 유용한 도구이지, 모든 컴파일러 IR이 반드시 따라야 하는 요구 사항은 아닙니다.

S, K, I: 바인더 없는 함수

콤비네이터(combinator)는 자유 변수가 없는 람다 항입니다. 콤비네이터는 주변으로부터 어떤 바인딩도 필요로 하지 않습니다. 세 개의 작은 콤비네이터만으로도 순수한 타입 없는 람다 대수의 함수들을 표현하기에 충분합니다:

I = \x -> x
K = \x -> \y -> x
S = \f -> \g -> \x -> f x (g x)
축약 규칙역할
I a → a인자를 그대로 통과시킵니다.
K a b → a첫 번째 인자를 유지합니다.
S f g a → f a (g a)적용의 양쪽 모두에 인자를 공급합니다.

이 대문자들은 수학적 이름입니다. 실행 가능한 테스트에서는 소문자 s, k, i를 평범한 지역 바인딩으로 사용합니다. 적용은 왼쪽으로 결합하므로 S f g a는 ((S f) g) a를 뜻합니다. 각 축약 규칙은 표시된 인자를 모두 받아야 발동할 수 있습니다.

I와 K는 쉽게 떠올릴 수 있습니다. S는 적용이 공유된 입력에 어떻게 의존할 수 있는지를 설명합니다. f, g, a가 주어지면 f a와 g a를 계산한 다음, 첫 번째 결과를 두 번째 결과에 적용합니다. f a (g a)의 괄호는 중요합니다.

두 함수 모두에 K를 골라 봅시다:

S K K a
→ K a (K a)        apply the S rule
→ a                apply the K rule

따라서 S K K는 I처럼 동작합니다. 아름답고 작은 결과입니다. 우리는 언뜻 보기에 항등 함수와는 전혀 닮지 않은 두 연산으로 항등 함수를 만들 수 있습니다. S와 K만으로 충분하지만, I를 남겨 두면 식을 읽기가 더 쉬워집니다. 이 장의 s k k (i 42) 예제를 실행해 보고 축약 과정을 따라가 보십시오.

왜 하필 이 연산들이 람다를 제거할 수 있을까요? 본문 t에 대해, x를 받아 t를 만들어 내는 콤비네이터 식을 [x]t로 씁시다. 안쪽 람다들이 변환되고 나면 세 가지 경우가 있습니다:

[x]x       = I
[x]t       = K t                  when x is not free in t
[x](p q)   = S ([x]p) ([x]q)       otherwise

본문이 매개변수 자체라면 그것을 반환합니다. 본문이 매개변수를 무시한다면 K를 사용해 매개변수를 버립니다. 그렇지 않으면 본문은 적용이며, S가 변환된 두 자식 모두에 매개변수를 전달합니다. 이 절차를 괄호 추상화(bracket abstraction)라고 합니다. 가장 안쪽 람다부터 바깥쪽으로 작업합니다. 닫힌 순수 람다 항을 변환하면 결국 콤비네이터와 적용만 남습니다.

예를 들어 f를 자유 이름으로 취급하여 \x -> f x에서 바인더를 제거해 봅시다:

[x](f x)
= S ([x]f) ([x]x)
= S (K f) I

결과에 인자를 하나 주어 확인해 봅시다:

S (K f) I a
→ (K f a) (I a)
→ f (I a)
→ f a

x의 출현이 사라졌습니다. 자유 변수 f는 당연히 그대로 남아 있습니다. 드 브루인 인덱스는 바인더를 유지하고 그 참조를 거리로 인코딩합니다. 브래킷 추상화는 바인더를 제거하고 바인더가 하던 일을 적용으로 표현합니다. 이로써 우리는 같은 계산의 두 표현 사이에서 일어나는 컴파일의 작은 예를 얻습니다.

함정이 하나 있는데, 그것은 우리가 처음에 다루었던 바로 그 평가 순서 문제입니다. 정규 순서는 omega가 영원히 실행되더라도 K 42 omega를 42로 축약합니다. 우리의 엄격한 평가기는 K의 본문에 들어가기 전에 omega를 계산하려 하므로 거기까지 도달하지 못합니다. 이 콤비네이터 등식들은 순수 대수에서의 재작성을 기술합니다. 이 등식들이 엄격한 컴파일러가 임의의 계산을 버려도 된다고 허용하는 것은 아닙니다. 우리는 바인딩과 축약을 이해하는 데 SKI를 사용하고, Fun의 백엔드는 클로저를 사용할 것입니다. 클로저를 쓰면 단순한 콤비네이터 변환이 만들어 낼 수 있는 거대한 적용 트리도 피할 수 있습니다.

구현

ast.rs는 람다, 적용, 불리언, 조건식을 추가합니다. 이 모듈의 프린터는 우선순위 문맥을 사용하며, 트리를 보존하는 데 필요한 곳에만 괄호를 추가합니다. 파서는 적용에 산술보다 높은 우선순위를 부여합니다:

Application: Expr = {
    <a:Application> <b:Atom> => {
        let (start, end) = (a.span.start, b.span.end);
        Expr::new(Kind::App(Box::new(a), Box::new(b)), start, end)
    },
    Atom,
};

names.rs는 자유 변수를 계산하고 포획을 피하는 치환을 수행합니다. 아래 헬퍼는 바인더 하나를 처리합니다. 재귀적인 치환은 각 항의 형태를 처리합니다. 새 이름은 원래 본문과 대체 항에 있는 자유 이름과 속박 이름을 모두 피하므로, 새 이름이 안쪽 람다와 충돌하지 않습니다.

#![allow(unused)]
fn main() {
fn binder(
    name: &str,
    body: &Expr,
    target: &str,
    value: &Expr,
) -> (String, Expr) {
    if name == target {
        return (name.to_owned(), body.clone());
    }
    let mut name = name.to_owned();
    let mut body = body.clone();
    if free(value).contains(&name) && free(&body).contains(target) {
        let mut used = BTreeSet::from([target.to_owned()]);
        all_names(&body, &mut used);
        all_names(value, &mut used);
        let mut fresh = format!("{name}'");
        while used.contains(&fresh) {
            fresh.push('\'');
        }
        let variable = Expr::new(Kind::Var(fresh.clone()), 0, 0);
        body = substitute(&body, &name, &variable);
        name = fresh;
    }
    (name, substitute(&body, target, value))
}
}

같은 모듈은 이름 붙은 항을 인덱스로 변환합니다. 스택은 가장 바깥 스코프부터 가장 안쪽 스코프까지의 이름을 담습니다. let의 우변은 그 바인더가 푸시되기 전에 변환됩니다. 그 바인더는 본문에만 속하기 때문입니다.

#![allow(unused)]
fn main() {
/// Bound names are distances to binders; free names remain explicit.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Term {
    Bound(usize),
    Free(String),
    Int(i64),
    Bool(bool),
    Lam(Box<Term>),
    App(Box<Term>, Box<Term>),
    Let(Box<Term>, Box<Term>),
    If(Box<Term>, Box<Term>, Box<Term>),
    Neg(Box<Term>),
    Binary(Op, Box<Term>, Box<Term>),
    Error,
}

/// Convert without substitution; alpha-equivalent expressions have equal terms.
pub fn debruijn(expr: &Expr, bound: &mut Vec<String>) -> Term {
    let mut go = |e: &Expr| Box::new(debruijn(e, bound));
    match &expr.kind {
        Kind::Var(n) => bound
            .iter()
            .rev()
            .position(|x| x == n)
            .map_or_else(|| Term::Free(n.clone()), Term::Bound),
        Kind::Lam(n, a) => {
            bound.push(n.clone());
            let body = debruijn(a, bound);
            bound.pop();
            Term::Lam(Box::new(body))
        }
        Kind::Let(n, a, b) => {
            let value = go(a);
            bound.push(n.clone());
            let body = debruijn(b, bound);
            bound.pop();
            Term::Let(value, Box::new(body))
        }
        Kind::App(a, b) => Term::App(go(a), go(b)),
        Kind::Binary(op, a, b) => Term::Binary(*op, go(a), go(b)),
        Kind::If(c, a, b) => Term::If(go(c), go(a), go(b)),
        Kind::Neg(a) => Term::Neg(go(a)),
        Kind::Int(n) => Term::Int(*n),
        Kind::Bool(b) => Term::Bool(*b),
        Kind::Error => Term::Error,
    }
}
}

normal.rs는 작은 한 단계를 구현합니다. 적용의 경우, 인자를 먼저 평가하지 않고 치환합니다. 함수가 람다가 아니면 먼저 그 함수를 한 단계 진행하고, 그보다 앞선 redex가 없을 때 인자를 진행합니다.

#![allow(unused)]
fn main() {
Kind::App(function, argument) => {
    if let Kind::Lam(name, body) = &function.kind {
        return Ok(Some(substitute(body, name, argument)));
    }
    if let Some(next) = step(function)? {
        Kind::App(Box::new(next), argument.clone())
    } else if let Some(next) = step(argument)? {
        Kind::App(function.clone(), Box::new(next))
    } else {
        return Ok(None);
    }
}
}

reduce는 한도와 각 항을 표시하는 콜백을 가지고 step을 반복해서 호출합니다. 이 축약기는 축약할 수 없는 적용을 포함한 열린 항도 살펴볼 수 있습니다. 이것은 Fun의 최종 실행 엔진이 아니라 항 재작성을 보여 주는 시연입니다.

반면 eval.rs는 람다의 환경을 클로저에 저장합니다:

#![allow(unused)]
fn main() {
/// Lexical bindings retained by a closure.
pub type Env = Vec<(String, Value)>;

#[derive(Clone, Debug)]
/// Runtime integers, booleans, and code paired with its lexical environment.
pub enum Value {
    Int(i64),
    Bool(bool),
    Closure(String, Rc<Expr>, Rc<Env>),
}
}

값에 의한 호출의 적용 경우는 그 순서를 명시적으로 드러냅니다:

#![allow(unused)]
fn main() {
Kind::App(function, argument) => {
    let function = evaluate(function, env, fuel)?;
    let argument = evaluate(argument, env, fuel)?;
    if let Value::Closure(name, body, captured) = function {
        let mut local = (*captured).clone();
        local.push((name, argument));
        evaluate(&body, &local, fuel)
    } else {
        Err("expected a function".into())
    }
}
}

본문은 캡처된 환경에 인자를 추가한 환경에서 실행됩니다. 재귀 방문마다 연료가 소모됩니다. CLI는 이러한 트리 순회기를 위해 더 큰 호스트 스택을 사용합니다. 5장에서는 호스트 재귀를 명시적 기계로 대체합니다. 연료도, 더 큰 스택도 그 변환을 대신할 수는 없습니다.

실행해 보기

$ cd crates/ch03_lambda
$ cargo run -q -- run tests/programs/church.fun
2
$ cargo run -q -- dump --pass=debruijn tests/programs/identity.fun
App(Lam(Bound(0)), Int(42))
$ cargo run -q -- repl
Lambda: :quit, :parse EXPR, :trace EXPR.
lambda> :trace (\x -> x) 42
(\x -> x) 42
42
lambda> :quit

run --engine=normal로 정규 순서를 선택하십시오. 끝나지 않을 수도 있는 항으로 실험할 때는 --fuel=N을 사용하십시오. 두 엔진은 서로 다른 연산을 세므로, 같은 연료 한도라도 두 엔진이 반드시 같은 지점에서 멈추는 것은 아닙니다. 실패하는 인자를 무시하는 함수를 시도해 보면 두 엔진의 답이 왜 다를 수 있는지 알 수 있습니다.

연습 문제

  1. 쉬움. \x -> \y -> y x를 손으로 드 브루인 인덱스로 변환하십시오.
  2. 보통. \y -> \y' -> x y에서 x를 자유 이름 y로 치환하십시오. 결과의 자유 변수를 확인하십시오.
  3. 보통. \x -> x x를 괄호 추상화(bracket abstraction)로 번역한 다음, 그 결과를 I에 적용하십시오. 그 축약 과정을 람다 항의 축약과 비교하십시오.
  4. 보통. K I a b를 추적하고 어느 인자가 버려지는지 설명하십시오.
  5. 어려움. 값에 의한 호출에 적합한 고정점 콤비네이터를 구성하고, 그 트레이스를 Y의 트레이스와 비교하십시오.

더 읽을거리

Pierce는 치환, 축약, 이름 없는 항을 다룹니다 (Pierce 2002). Felleisen과 Friedman은 우리가 5장에서 다시 다룰 추상 기계 설정을 소개합니다 (Felleisen and Friedman 1986).

4장: 단순 타입 람다 대수

새 파일

  • src/types.rs

변경된 파일

  • src/ast.rs
  • src/eval.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/names.rs
  • src/normal.rs

동기

우리의 평가기는 1 True를 기꺼이 실행하려 듭니다. 평가기는 결국 정수가 함수가 아니라는 사실을 알아내지만, 이 실수를 발견하려고 프로그램을 실행할 필요는 없었습니다. 컴파일러가 이런 실수를 대신 잡아 줄 수는 없을까요?

타입 검사기가 하는 일이 바로 이런 종류의 추론입니다. 함수 타입은 함수가 무엇을 받고 무엇을 반환하는지 알려 줍니다. 우리는 이 타입들을 넘기려는 인자와 비교하여 호출을 검사합니다. 규칙은 적어 내려갈 수 있을 만큼 작고, Rust 구현도 이 규칙을 충실히 따를 것입니다.

당분간은 프로그래머에게 람다 매개변수에 타입을 표기해 달라고 요청하겠습니다. 이렇게 하면 명확한 출발점이 생깁니다. 6장에서는 컴파일러가 스스로 타입을 알아내도록 가르칠 것이며, 여기에는 여러 종류의 값에 두루 쓰이는 타입도 포함됩니다.

현재의 언어

타입은 Int, Bool, 그리고 화살표입니다. 화살표는 오른쪽으로 결합하므로 Int -> Bool -> Int는 Int -> (Bool -> Int)를 뜻합니다. 왼쪽에 오는 화살표에는 괄호가 필요합니다.

let choose = \(b : Bool) -> \(x : Int) -> \(y : Int) ->
  if b then x else y
in choose True 10 20

모든 람다 매개변수에 타입을 표기하지만, 호출, 산술, 스코프는 이전과 똑같이 동작합니다. 정수를 함수처럼 다시 호출해 보십시오. 이번에는 평가기가 관여하기 전에 검사기에서 E0201이 나옵니다. 우리는 실수를 발견하는 시점을 과정의 더 앞쪽으로 옮겼습니다.

이론

타입이 있는 문법

새로 추가되는 요소는 각 람다 매개변수에 붙는 타입입니다. 먼저 우리가 쓸 수 있는 타입은 다음과 같습니다. 함수의 입력과 출력 자체가 함수일 수도 있습니다.

식은 앞 장에서 이미 본 것들입니다. 의 표기는 검사기에게 에 대해 무엇을 가정해도 되는지 알려 줍니다.

앞에서와 마찬가지로 은 정수이고 는 +, -, *, /, ==, <, <= 중 하나입니다. 소스 스팬과 파서의 오류 복구용 자리 표시자는 생략합니다. 이 문법은 우리가 파싱할 수 있는 것을 기술합니다. 아래의 타입 규칙은 그런 식 가운데 어떤 것을 안전하게 실행할 수 있는지 결정합니다. 예를 들어 1 True는 여전히 적용의 모양을 갖추고 있지만, 타입 유도는 존재하지 않습니다.

타입 부여와 안전성

타입 환경은 이름을 타입에 대응시킵니다. 변수를 읽을 때는 가장 가까운 바인딩을 찾습니다. 람다는 본문을 검사하는 동안 그 환경을 확장합니다. 적용은 인자를 함수의 정의역과 대조하여 검사합니다 (Pierce 2002).

if의 조건은 Bool 타입입니다. 두 분기의 타입은 같습니다. 산술 연산은 정수를 받고, 비교 연산은 불리언을 반환합니다. let은 환경을 확장하기 전에 우변을 검사하므로 재귀적이지 않은 상태로 남습니다. 암묵적인 양화(quantification)는 없습니다. Int -> Int로 표기된 항등 함수는 Bool까지 받을 수는 없습니다.

우리가 얻은 것을 설명해 주는 유용한 개념이 두 가지 있습니다. 보존성(preservation)은 평가 단계를 한 번 밟아도 식의 타입이 바뀌지 않는다는 성질입니다. Int 식에서 시작했다면, 그것을 축약했을 때 갑자기 함수가 나와서는 안 됩니다. 람다 호출에 대한 증명의 핵심 단계는 올바르게 타입이 매겨진 인자를 치환해도 본문의 타입이 그대로 유지된다는 것을 보이는 것입니다. 이것이 바로 치환 보조정리입니다.

진행(progress)은 닫혀 있고 타입이 올바른 식이 값이거나 한 단계 더 진행할 수 있다고 말합니다. 우리는 42를 호출하는 것과 같은 무의미한 명령에 도달하지 않습니다. 0으로 나누면 여전히 런타임 오류가 보고될 수 있으므로, 우리의 나눗셈 프리미티브에는 약간의 단서가 필요합니다. 타입은 이 언어의 형태 실수를 잡아냅니다. 타입이 모든 계산의 성공을 약속하지는 않습니다. Pierce는 이러한 주장 뒤에 있는 증명을 전개합니다 (Pierce 2002).

구현

types.rs는 화살표를 재귀적으로 표현하고, 필요한 괄호와 함께 출력합니다. 아직 메타변수나 치환 맵은 없습니다.

#![allow(unused)]
fn main() {
/// Simple types have no variables, schemes, or implicit quantification.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Type {
    Int,
    Bool,
    Arrow(Box<Type>, Box<Type>),
}

impl Type {
    pub fn arrow(domain: Self, codomain: Self) -> Self {
        Self::Arrow(Box::new(domain), Box::new(codomain))
    }
}

impl fmt::Display for Type {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Int => write!(f, "Int"),
            Self::Bool => write!(f, "Bool"),
            Self::Arrow(a, b) => {
                if matches!(**a, Self::Arrow(..)) {
                    write!(f, "({a})")?;
                } else {
                    write!(f, "{a}")?;
                }
                write!(f, " -> {b}")
            }
        }
    }
}
}

문법은 화살표의 공역에 오른쪽 재귀를 사용합니다. 이제 AST의 람다 변형은 매개변수 이름 옆에 Type을 저장하므로, 파싱에 성공한 람다에는 항상 타입 표기가 붙어 있습니다.

Type: Type = {
    <a:TypeAtom> "->" <b:Type> => Type::arrow(a, b),
    TypeAtom,
};
TypeAtom: Type = {
    "Int" => Type::Int, "Bool" => Type::Bool, "(" <Type> ")",
};

검사기는 타입 규칙을 그대로 반영합니다. 검사기의 환경은 스택이며, 가장 안쪽 바인딩부터 바깥쪽으로 탐색합니다. 본문 검사가 오류를 반환하더라도 스코프는 꺼내집니다. 적용은 먼저 함수 타입을 찾은 다음 인자를 검사하므로, 호출 전체가 아니라 인자 위치에서 유용한 오류를 보고합니다.

#![allow(unused)]
fn main() {
fn infer(
    expr: &Expr,
    env: &mut Vec<(String, Type)>,
) -> Result<Type, Diagnostic> {
    match &expr.kind {
        Kind::Int(_) => Ok(Type::Int),
        Kind::Bool(_) => Ok(Type::Bool),
        Kind::Var(name) => env
            .iter()
            .rev()
            .find(|(n, _)| n == name)
            .map(|(_, ty)| ty.clone())
            .ok_or_else(|| {
                Diagnostic::new(
                    "E0101",
                    format!("unbound variable {name}"),
                    expr.span.clone(),
                )
            }),
        Kind::Lam(name, parameter, body) => {
            env.push((name.clone(), parameter.clone()));
            let result = infer(body, env);
            env.pop();
            Ok(Type::arrow(parameter.clone(), result?))
        }
        Kind::App(function, argument) => {
            let function_ty = infer(function, env)?;
            let Type::Arrow(domain, codomain) = function_ty else {
                return Err(mismatch(
                    function,
                    "a function type",
                    &function_ty,
                ));
            };
            expect(argument, infer(argument, env)?, &domain)?;
            Ok(*codomain)
        }
        Kind::Let(name, value, body) => {
            let ty = infer(value, env)?;
            env.push((name.clone(), ty));
            let result = infer(body, env);
            env.pop();
            result
        }
        Kind::If(condition, yes, no) => {
            expect(condition, infer(condition, env)?, &Type::Bool)?;
            let ty = infer(yes, env)?;
            expect(no, infer(no, env)?, &ty)?;
            Ok(ty)
        }
        Kind::Neg(value) => {
            expect(value, infer(value, env)?, &Type::Int)?;
            Ok(Type::Int)
        }
        Kind::Binary(op, left, right) => {
            expect(left, infer(left, env)?, &Type::Int)?;
            expect(right, infer(right, env)?, &Type::Int)?;
            Ok(match op {
                Op::Eq | Op::Lt | Op::Le => Type::Bool,
                _ => Type::Int,
            })
        }
        Kind::Error => Err(Diagnostic::new(
            "E0002",
            "unrecovered syntax error",
            expr.span.clone(),
        )),
    }
}
}

작은 expect 도우미는 두 단순 타입을 구조적으로 비교합니다. 불일치가 있으면, 식의 바이트 범위에서 기대한 타입과 발견한 타입을 모두 보고합니다. Ariadne가 소스 발췌를 렌더링합니다. 이 독립형 검사기는 첫 번째 타입 오류에서 멈춥니다. 앞선 어휘 패스와 스코프 패스는 여전히 오류를 수집합니다.

평가기는 표기를 무시하며 3장의 클로저 표현을 그대로 유지합니다. 치환, 정규 순서 축약, 드 브루인 변환은 람다를 처리하는 부분에서 타입을 함께 전달합니다. 덕분에 보존 테스트가 모든 중간 항을 검사할 수 있고, 이름 없는 덤프에서도 주석이 달린 항들이 서로 구별됩니다.

실행해 보기

$ cd crates/ch04_stlc
$ cargo run -q -- run tests/programs/typed-return.fun
42
$ cargo run -q -- dump --pass=types tests/programs/church.fun
Int
$ cargo run -q -- repl
STLC: :quit, :type EXPR, :parse EXPR, :trace EXPR.
stlc> :type \(f : Int -> Bool) -> \(x : Int) -> f x
(Int -> Bool) -> Int -> Bool
stlc> :quit

check는 평가하지 않고 검사만 합니다. 두 실행 엔진 모두 먼저 타입을 검사합니다. :trace도 축약 과정을 표시하기 전에 타입이 잘못된 입력을 거부합니다. 1 / 0의 검사가 성공하는 것은 예상된 결과입니다. 실행은 실패하지만, 그 타입은 Int입니다.

연습 문제

  1. 쉬움. 추상화 규칙을 사용하여 choose 예제의 타입을 유도하십시오.
  2. 쉬움. (Int -> Bool) -> Int에 괄호가 필요한 이유를 설명하십시오.
  3. 보통. 타입이 올바른 함수에 잘못된 타입의 인자를 적용하는 식을 작성하십시오. 진단의 스팬을 확인하십시오.
  4. 어려움. 이 계산 체계의 치환 보조정리를 대체 항에 대한 조건까지 포함하여 진술하고, 람다 경우를 직접 증명해 보십시오.

더 읽을거리

Pierce는 진행(progress) 정리와 보존(preservation) 정리의 형식적 증명을 제시합니다(Pierce 2002). Damas와 Milner는 단순 타입을 넘어서는 다음 단계, 즉 주 타입 스킴과 다형적 let이 왜 필요한지를 설명합니다(Damas and Milner 1982). Takenobu Tani의 Type Introduction Illustrated for Casual Haskellers(pdf / github)는 단순 타입부터 GHC가 추가하는 확장에 이르기까지 같은 개념을 그림으로 보여 줍니다.

5장: 평가

새 파일

  • src/cek.rs
  • src/cbn.rs

변경된 파일

  • src/eval.rs
  • src/types.rs
  • src/ast.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/names.rs
  • src/normal.rs

동기

재귀 평가기는 평가를 설명하기에 아주 좋은 방법입니다. Rust 호출 하나하나가 프로그램의 한 조각을 더 따라갑니다. 안타깝게도 호출 하나하나가 Rust 스택을 한 조각씩 더 쓸 수도 있습니다. 우리의 작은 언어는 우리가 전혀 의도하지 않은 한계를 물려받은 셈입니다. 스택을 키우면 시간을 벌 수는 있지만, 문제는 여전히 남아 있습니다.

우리는 평가기가 다음에 해야 할 일을 명시적으로 적어 두는 방식으로 이 문제를 해결할 것입니다. 그 정보를 Rust 호출 프레임에 맡겨 두는 대신 자료 구조에 저장하고, 평가기 전체를 하나의 루프로 구동할 것입니다. 이것이 추상 기계의 바탕에 깔린 아이디어입니다.

우리가 사용할 기계는 CEK라고 부릅니다. C는 무엇을 평가하고 있는지 알려 주고, E는 현재 변수 바인딩을 기록하며, K는 다음에 올 일을 기록합니다 (Felleisen and Friedman 1986). 이름이 아이디어보다 더 위압적으로 들릴 뿐입니다. 우리는 평가기가 암묵적으로 하던 장부 정리를 우리가 들여다볼 수 있는 무언가로 바꾸고 있습니다.

현재의 언어

재귀 바인딩은 우변보다 앞에 자신의 타입을 적습니다. 그 우변은 구문상 람다여야 합니다. let rec x : Int = x in x 같은 값은 E0120으로 거부됩니다.

let rec sum : Int -> Int -> Int =
  \(acc : Int) -> \(n : Int) ->
    if n == 0 then acc else sum (acc + n) (n - 1)
in sum 0 100

자기 자신의 이름은 함수 내부와 그 함수를 감싸는 let 본문 안에서 사용할 수 있습니다. 그 타입 표기는 추론된 우변 타입과 대조하여 검사됩니다. fix 역시 명시적인 항 형태이며, 적용보다 더 강하게 결합합니다:

fix (\(self : Int -> Int) -> \(n : Int) ->
  if n == 0 then 1 else n * self (n - 1)) 6

두 예제 모두 타입이 올바르지만, 꼬리 재귀인 것은 합계뿐입니다. 곱셈은 재귀적인 팩토리얼 호출이 반환될 때마다 그 결과를 기다려야 합니다.

이론

함수의 고정점은 펼침 등식(unfolding equation)을 만족합니다. fix f는 f (fix f)처럼 동작합니다. 그 타입 규칙은 인자로 받는 함수가 입력 타입과 같은 타입을 반환하도록 요구합니다 (Pierce 2002).

fix가 있으면 타입이 올바른 프로그램도 영원히 실행될 수 있습니다. fix (\(x : Int) -> x)를 실행해 보고, 연료(fuel)를 얼마나 주든 이 프로그램이 모두 소진하는 것을 확인해 보십시오. 타입 검사기가 우리를 저버린 것은 아닙니다. 타입 검사기는 이 재귀 프로그램이 종료한다고 약속한 적이 없습니다. 재귀 let도 같은 고정점 개념을 사용하지만, 어떻게 초기화할지 알 수 있도록 우변에 함수가 와야 합니다. Fun은 이 제약을 유지할 것입니다.

값에 의한 호출(call-by-value)은 함수 본문에 들어가기 전에 함수와 인자를 먼저 평가합니다. 이름에 의한 호출(call-by-name)은 평가하지 않은 인자를 그것이 쓰일 때마다 치환하며, 람다에 이르면 멈춥니다. 필요에 의한 호출(call-by-need)은 여기에 더해 인자의 결과를 공유하므로, 보류된 계산과 메모된 결과가 필요합니다. 비교를 위한 우리의 작은 인터프리터는 메모이제이션 없이 이름에 의한 호출을 구현합니다. 그 치환 코드를 보면 공유가 없다는 점이 명시적으로 드러납니다. 3장의 완전한 정규 순서(normal-order) 축약기는 이 약한(weak) 이름에 의한 호출 평가기와 달리 람다 본문 안으로도 들어갑니다.

Fun은 중간 언어들을 거치는 동안 평가 순서와 할당을 더 쉽게 따라갈 수 있도록 값에 의한 호출을 택합니다. 생성자 역시 엄격(strict)할 것입니다. 람다는 여전히 값이므로, 언어는 어떤 동작을 아직 수행하지 않은 채로 표현할 수 있습니다. 15장에서는 이 성질을 IO에 활용합니다. 지연 평가를 도입하려면 또 다른 런타임 표현과, 효과와 공간 사용에 대한 또 다른 설명이 필요한데, 이는 이 책이 택한 컴파일러의 범위를 넘어섭니다.

구현

파서는 재귀 바인딩 제약을 검사하고, 허용된 바인딩을 다음 생성자를 통해 하위 표현으로 변환합니다:

#![allow(unused)]
fn main() {
/// Lower function-only recursive bindings to a fixed point with an annotated self parameter.
pub fn recursive(
    name: String,
    ty: Type,
    rhs: Expr,
    body: Expr,
    start: usize,
    end: usize,
) -> Result<Expr, Diagnostic> {
    if !matches!(rhs.kind, Kind::Lam(..)) {
        return Err(Diagnostic::new(
            "E0120",
            "recursive binding must be a function",
            rhs.span,
        ));
    }
    let self_function =
        Expr::new(Kind::Lam(name.clone(), ty, Box::new(rhs)), start, end);
    let fixed = Expr::new(Kind::Fix(Box::new(self_function)), start, end);
    Ok(Expr::new(
        Kind::Let(name, Box::new(fixed), Box::new(body)),
        start,
        end,
    ))
}
}

Value::Recursive는 고정점을 구성하는 데 쓰이는 함수를 저장합니다. 그 재귀 변수를 읽으면 저장된 함수를 해당 재귀 참조에 적용합니다. 재귀 함수의 경우 이는 원래 환경에서 클로저를 다시 만들어 냅니다. 이 방식은 Rust 소유권 순환을 만들지 않습니다. fix에 진입하면 그것을 정의하는 함수를 즉시 평가하므로, 그 초기화 과정에서 발생하는 오류는 여전히 엄격하게 드러납니다. 이것은 명시적 재귀를 보여 주기 위한 교육용 구현이며, 범용 지연 바인딩 메커니즘이 아닙니다.

cek.rs는 제어를 컨티뉴에이션 프레임과 분리합니다. 프레임 변형들은 트리 평가기라면 재귀 호출을 기다려야 했을 지점들입니다.

#![allow(unused)]
fn main() {
/// The remaining work after the current expression returns a value.
enum Frame {
    Argument(Rc<Expr>, Rc<Env>),
    Apply(Value),
    Let(String, Rc<Expr>, Rc<Env>),
    If(Rc<Expr>, Rc<Expr>, Rc<Env>),
    Neg,
    Left(Op, Rc<Expr>, Rc<Env>),
    Right(Op, i64),
    Fix,
}

/// Control alternates between evaluating an expression and returning a value.
enum Control {
    Expr(Rc<Expr>, Rc<Env>),
    Value(Value),
    Apply(Value, Value),
}

/// Statistics make tail-space behavior testable without relying on timing.
pub struct ResultValue {
    pub value: Value,
    pub steps: usize,
    pub max_continuations: usize,
}
}

적용은 먼저 함수를 평가하는 동안 Argument 프레임을 푸시합니다. 그다음 인자를 평가하는 동안 그 프레임은 Apply 프레임이 됩니다. 두 값이 모두 준비되면, 기계는 클로저의 캡처된 환경을 매개변수로 확장한 환경에서 본문에 진입합니다. 이때 적용 프레임들은 이미 팝된 상태입니다:

#![allow(unused)]
fn main() {
Control::Apply(
    Value::Closure(name, body, captured),
    argument,
) => {
    let mut env = (*captured).clone();
    env.push((name, argument));
    Control::Expr(body, Rc::new(env))
}
Control::Apply(_, _) => {
    return Err("expected a function".into());
}
}

합계 함수를 다시 봅시다. 이 함수의 재귀 호출은 이미 답을 만들어 내므로, 호출자를 위해 기억해 둘 것이 없습니다. 팩토리얼에는 아직 끝내야 할 곱셈이 남아 있습니다. 그 보류된 작업에는 프레임이 필요합니다.

이제 두 버전 모두 Rust의 호출 스택을 키우지 않습니다. 그중 꼬리 재귀 버전만이 우리의 명시적인 컨티뉴에이션 스택까지 커지지 않게 합니다. 우리는 짧은 실행과 백만 번 호출하는 실행의 최대 컨티뉴에이션 깊이를 비교하여 이를 확인합니다. 넉넉한 크기의 기계 한 대에서 실행이 끝났다는 사실만으로는 그만큼 많은 것을 알려 주지 못합니다.

전체 전이 루프는 식의 형태를 따라가며, 값이 반환될 때 프레임 하나를 팝합니다. max_continuations는 매 전이 전에 측정되며, 연료 카운터 덕분에 발산하는 예제도 안전하게 살펴볼 수 있습니다. 연료는 기계 전이 횟수를 세므로, 그 수치는 트리 평가기가 세는 식 방문 횟수와 다릅니다.

작은 이름에 의한 호출(call-by-name) 평가기는 적용과 let 경우에 환경 대신 치환을 사용합니다:

#![allow(unused)]
fn main() {
Kind::App(f, a) => {
    let function = evaluate(f, fuel)?;
    if let Kind::Lam(name, _, body) = function.kind {
        evaluate(&substitute(&body, &name, a), fuel)
    } else {
        Err("expected a function".into())
    }
}
Kind::Let(n, a, b) => evaluate(&substitute(b, n, a), fuel),
}

필요에 의한 호출(call-by-need) 변형이라면 이 치환을 공유 셀로 대체할 것이며, 그 셀은 처음 요구될 때 인자를 계산하고 저장합니다. 그 구현은 향후 과제로 남겨 둡니다. 여기서 실제 운영 경로는 여전히 엄격한 기계이며, 비교용 인터프리터는 명시적으로 선택해야 합니다.

실행해 보기

$ cd crates/ch05_eval
$ cargo run -q -- run tests/programs/sum-tail.fun
5050
$ cargo run -q -- run --engine=core tests/programs/factorial.fun
720
$ cargo run -q -- dump --pass=types tests/programs/fix-factorial.fun
Int
$ cargo run -q -- run --engine=cbn tests/programs/strict-argument.fun
1
$ cargo test -q a_million_tail_calls_use_bounded_continuations

기본 엔진은 cek입니다. core는 재귀 클로저 평가기를, cbn은 비교용 인터프리터를 선택하며, normal은 앞서 만든 완전 축약기를 그대로 사용합니다. --fuel=N은 실행 횟수에 상한을 둡니다. 트리 인터프리터에는 적당한 연료 예산을 사용하십시오. 그것들의 호스트 스택 의존성을 없애는 것은 CEK입니다. 두 엄격한 엔진은 답과 실패 모두에서 일치해야 합니다. 이름에 의한 호출 비교 구현은 사용되지 않는 인자가 실패할 경우 의도적으로 다르게 동작합니다. 구현을 읽기 전에 실행해 보고, 어떤 오류가 나올지, 혹은 오류가 나오기는 할지 예측해 보십시오.

연습 문제

  1. 쉬움. 각 피연산자가 반환될 때마다 1 + 2 * 3의 컨티뉴에이션 프레임을 그려 보십시오.
  2. 보통. 꼬리 합을 꼬리가 아닌 합으로 바꾸고 두 경우의 최대 컨티뉴에이션 깊이를 측정해 보십시오.
  3. 보통. 클로저가 호출자의 환경이 아니라 정의된 곳의 환경을 캡처해야 하는 이유를 설명하십시오.
  4. 어려움. 값에 의한 호출(call-by-value)은 한 번만 계산하는데 이름에 의한 호출(call-by-name)은 상당한 작업을 반복하게 되는 식을 제시하십시오. 공유(sharing)가 무엇을 바꾸는지 설명하십시오.

더 읽을거리

Felleisen과 Friedman은 추상 기계 관점의 설명을 전개합니다 (Felleisen and Friedman 1986). Pierce는 평가 전략, 타입 지정, 고정점 사이의 관계를 다룹니다 (Pierce 2002). Appel은 컴파일 과정에서 컨티뉴에이션을 명시적으로 만드는 관련 기법을 전개합니다(Appel 1992). 우리의 ANF 컴파일러는 나중에 이 연결 고리를 다시 다룹니다.

6장: 힌들리–밀너 추론

새 파일

  • src/algorithm_w.rs
  • src/union_find.rs

변경된 파일

  • src/types.rs
  • src/ast.rs
  • src/cbn.rs
  • src/cek.rs
  • src/eval.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/names.rs
  • src/normal.rs

동기

\x -> x를 봅시다. 우리는 x가 어떤 타입인지 말하지 않았지만, 이미 유용한 사실을 하나 알고 있습니다. 무엇이 들어가든 정확히 그것이 그대로 다시 나옵니다. 따라서 이 함수의 타입은 a -> a 꼴이어야 합니다.

꽤 멋지지 않습니까! 우리는 함수를 한 번도 실행하지 않고 가능한 모든 호출에 대해 무언가를 알아냈습니다. 정수든, 불리언이든, 또 다른 함수든 모두 동작합니다. 타입 스킴(type scheme)은 각 사용처가 저마다의 a를 고를 수 있게 해 줍니다. 힌들리–밀너 추론은 프로그램이 허용하는 가장 일반적인 스킴을 찾아냅니다 (Damas and Milner 1982).

우리는 이것을 두 가지 방식으로 구현할 것입니다. 알고리즘 W는 자신이 발견한 타입 치환을 명시적으로 기록해 둡니다 (Damas and Milner 1982). 두 번째 구현은 가변 셀을 갱신하고, 레벨을 사용해 어떤 변수를 일반화할 수 있는지 추적합니다. 두 구현은 같은 방정식을 풀지만, 구현상의 선택은 서로 다릅니다. 두 가지를 모두 직접 따라가 보면, Fun에 가져갈 버전을 고르기 전에 알고리즘을 이해하는 데 도움이 될 것입니다.

현재의 언어

Poly에는 주석 없는 람다, 적용, 다형적 let, 함수 전용 let rec, 정수, 불리언, 조건식, 산술이 있습니다. 비교용 연습 문제를 위해 평가 장에서 다룬 명시적 fix 형식은 여전히 사용할 수 있습니다.

let id = \x -> x in
let first = id 1 in
id True

id가 두 번 나타날 때 각각은 그 스킴을 독립적으로 인스턴스화합니다. 람다 매개변수는 다르게 동작합니다. \f -> let first = f 1 in f True는 거부되는데, 그 람다 본문 전체에서 f가 하나의 타입만 가지기 때문입니다.

우리는 이전 장의 매개변수 표기를 생략할 수 있습니다. 특히 항등 함수는 더 이상 정수를 받을지 불리언을 받을지 선택하지 않아도 됩니다. 우리는 이미 이해하고 있는 평가 장치는 그대로 두고, 컴파일러가 그 자유를 어떻게 알아내는지에 집중하겠습니다.

이론

타입 규칙 읽는 법

타입 규칙으로 가득한 페이지는 처음 볼 때 위압적으로 보일 수 있습니다. 이 표기법은 대체로 같은 문장을 되풀이하지 않게 해 주려고 있는 것입니다. 각 규칙은 구문의 한 조각을 설명합니다. 먼저 가로줄 아래를 보고 우리가 무엇을 확립하려는지 확인한 다음, 줄 위를 보고 각 부분에 대해 무엇을 알아야 하는지 확인하십시오. Pierce는 타입 시스템을 다루는 책 전반에서 이 방식을 사용합니다 (Pierce 2002).

이것은 “감마에 있는 가정 아래에서 식 e는 타입 tau를 가진다”라고 읽습니다. 이 기호들 덕분에 반복되는 문장을 짧게 쓸 수 있습니다:

표기법의미
x, 42, f x 같은 프로그램 식입니다.
, , 타입, 또는 타입을 나타내는 타입 변수입니다.
타입 스킴으로, 타입 변수를 한정할 수도 있습니다.
타입 환경입니다. 이름과, 그 이름에 대해 우리가 알고 있는 타입이나 스킴을 담습니다.
x에 대한 지역 가정으로 환경을 확장합니다.
“이 가정들 아래에서 우리는 다음을 확립할 수 있습니다 …”
를 받아 를 돌려주는 함수입니다.
사용하는 쪽이 를 대체할 타입을 고를 수 있는 스킴입니다.

콜론은 타입을 단언합니다. 식을 평가하지는 않습니다. Gamma에는 런타임 값이 아니라 정적인 가정이 들어 있습니다. 이 장에서 Gamma는 let으로 묶인 이름을 스킴에, 람다 매개변수를 단형 타입에 대응시키며, 단형 타입은 양화자가 없는 스킴으로 표현됩니다. 규칙에 나오는 그리스 문자 변수는 수학적 자리 표시자입니다. 추론기의 가변 메타변수는 어떤 타입이 규칙을 만족하는지 찾아내기 위한 구현 기법입니다.

가로 막대는 “위에 있는 것이 모두 성립하면 아래의 명제도 성립한다”는 뜻입니다. 나눗셈이 아닙니다. 윗부분이 비어 있는 규칙에는 전제가 없습니다. 정수 리터럴은 하위 식을 살펴보지 않고도 Int 타입을 가집니다.

적용의 경우, 두 사실이 같은 입력 타입에 대해 일치해야 합니다:

e1이 정수를 받아 불리언을 반환한다면, e2는 정수여야 하고 호출은 불리언을 반환합니다. 반복되는 가 그 일치를 표현합니다. 추론 과정에서 우리는 이를 단일화로 알아냅니다. 규칙은 유효한 관계를 기술합니다. 전제를 어떤 순서로 풀어야 하는지 정해 주지는 않습니다.

작은 유도 만들기

\x -> x + 1을 생각해 봅시다. x에 임시 타입을 부여하십시오. 덧셈 규칙은 두 피연산자가 모두 정수일 것을 요구하므로, 그 임시 타입은 Int로 정해집니다. 로 쓰면 결과 트리를 간결하게 유지할 수 있습니다:

두 잎은 조회와 리터럴 규칙으로 Add를 정당화합니다. 그런 다음 람다 규칙이 매개변수에 관한 가정을 함수 타입 안으로 옮깁니다:

여기서 는 \x -> e를 뜻합니다. Lam은 우리의 함수 타입 Int -> Int를 부여하고, 본문이 x에 대해 가졌던 지역 가정을 제거합니다. 1을 True로 바꾸면 Add의 전제가 깨지므로, 유도를 완성할 수 없습니다.

단형 타입에서 스킴으로

let id = \x -> x in ...에서 흥미로운 점은 id를 사용하는 곳마다 각자 자기 인자 타입을 고를 수 있다는 것입니다. 우리는 이 허용을 스킴(scheme)에 기록합니다. 단형 타입(monotype)은 하나의 타입을 기술하며, 아직 풀지 않은 변수를 포함할 수도 있습니다. 스킴은 호출하는 쪽이 어떤 변수를 새로 고를 수 있는지 표시합니다. 그렇게 새 사본을 만드는 일이 인스턴스화(instantiation)입니다. 어떤 변수를 호출하는 쪽에 안전하게 내줄 수 있는지 정하는 일이 일반화(generalization)입니다 (Damas and Milner 1982).

둘러싼 스코프가 아직 의존하고 있는 변수는 일반화할 수 없습니다. 그렇지 않으면 지역 함수가 어떤 타입이든 받는 척하면서, 타입이 이미 고정된 바깥 값을 몰래 사용할 수 있게 됩니다. 따라서 람다 매개변수는 모든 사용처가 공유하는 단형 타입 하나로 시작합니다.

Let 규칙은 세 단계로 읽으십시오. 먼저 바인딩의 우변을 추론하고, 그 타입을 일반화합니다. 마지막으로 그 스킴을 스코프에 둔 채 본문을 검사합니다. 에 대한 등식은 평가할 또 하나의 프로그램이 아니라, 그 스킴을 정의하는 부수 조건입니다. 타입이나 환경 안의 변수가 스킴의 양화사에 묶여 있지 않으면 그 변수는 자유(free) 변수입니다. 환경의 자유 변수에 들어 있지 않은 변수만 일반화할 수 있습니다.

let id = \x -> x in ...에서 우변의 타입은 입니다. 가 둘러싼 환경과 무관하다면, 우리는 id를 에 바인딩합니다. 이름을 조회할 때마다 이 스킴을 새로 인스턴스화합니다. id 1은 Int를 고릅니다. id True는 Bool을 고릅니다. 반면 \f -> ...의 매개변수 f는 Lam 규칙에서 단형 타입 하나를 받습니다. 그 모든 사용처가 같은 타입을 만족해야 합니다. 양화는 let 규칙의 몫이지, 우연히 함수를 가리키는 모든 이름의 몫이 아닙니다.

단일화는 타입 사이의 동등성 제약을 풉니다. 두 화살표 타입은 정의역을 단일화하고, 거기서 알아낸 것을 적용한 다음, 공역을 단일화하는 방식으로 단일화됩니다. 변수는 어떤 타입 안에 나타나지 않을 때에만 그 타입으로 풀 수 있습니다. 이 발생 검사가 없으면 자기 적용에는 무한 타입이 필요해집니다. 이 오류는 E0202로 보고됩니다. 호환되지 않는 기본 타입은 E0201을 냅니다.

재귀 함수는 본문을 검사하는 동안 하나의 임시 단형 타입을 가집니다. 그 검사가 끝난 뒤에야 바깥의 let이 그것을 일반화할 수 있습니다. 우리가 fix로 낮추는 방식은 평범한 단형 자기 매개변수를 통해 이 규칙을 표현합니다. 여기에는 다형 재귀(polymorphic recursion)가 없습니다. 환경 기반 일반화 규칙과 그 효율적인 레벨 기반 형태는 인용한 문헌에서 전개됩니다 [Kiselyov (2013); Rémy (1992)].

Poly에는 가변 상태가 없으므로, let의 우변은 일반화되기 위해 구문적 값일 필요가 없습니다. 예를 들어, 항등 함수를 다른 항등 함수에 적용해도 여전히 다형적 바인딩이 만들어집니다. 이 제한 없는 규칙은 Fun에도 적용될 것입니다.

작은 보상

합성은 짧은 정의가 얼마나 많은 것을 말할 수 있는지 보여 주는 멋진 예입니다.

let compose = \f -> \g -> \x -> f (g x) in
compose (\x -> x + 1) (\x -> x * 2) 20

결과는 41입니다. 더 흥미로운 점은 compose 자체는 정수에 대해 아무것도 모른다는 것입니다. 그 주 타입은 (b -> c) -> (a -> b) -> a -> c입니다. g의 출력이 f의 입력이므로, 그 두 위치는 b를 공유합니다. a, b, c가 같은 타입이어야 한다고 요구하는 것은 아무것도 없습니다. 정의 안의 두 번의 적용만으로 이 이야기 전체를 얻을 수 있습니다. 바로 이런 점 때문에 추론은 만들어 볼 가치가 있습니다.

구현

두 알고리즘은 불변인 출력 타입과 타입 스킴은 공유하지만, 단일화자나 추론 순회는 공유하지 않습니다. 변수의 정체성은 추론이 끝난 뒤에야 첫 등장 순서에 따라 정규화됩니다. 그 덕분에 두 알고리즘의 주 타입을 이름 바꾸기를 무시하고 비교할 수 있습니다.

#![allow(unused)]
fn main() {
/// Variables are fresh identities; algorithms choose their own representation of solutions.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Type {
    Int,
    Bool,
    Var(u32),
    Arrow(Box<Type>, Box<Type>),
}

/// Quantified variables are freshened independently at every occurrence.
#[derive(Clone, Debug)]
pub struct Scheme {
    pub variables: Vec<u32>,
    pub ty: Type,
}

impl Scheme {
    pub fn mono(ty: Type) -> Self {
        Self {
            variables: vec![],
            ty,
        }
    }
}

impl Type {
    pub fn arrow(a: Self, b: Self) -> Self {
        Self::Arrow(Box::new(a), Box::new(b))
    }
    /// Rename by first occurrence, making principal types comparable up to alpha-renaming.
    pub fn canonical(&self) -> Self {
        fn go(ty: &Type, names: &mut BTreeMap<u32, u32>) -> Type {
            match ty {
                Type::Var(n) => {
                    let next = names.len() as u32;
                    Type::Var(*names.entry(*n).or_insert(next))
                }
                Type::Arrow(a, b) => {
                    Type::arrow(go(a, names), go(b, names))
                }
                other => other.clone(),
            }
        }
        go(self, &mut BTreeMap::new())
    }
}
}

알고리즘 W

algorithm_w.rs는 치환, 인스턴스화, 일반화, 추론을 분리합니다. 핵심 규칙은 Poly의 산술, 조건, 고정점 형식으로 확장되며, 진단을 위해 소스 스팬이 유지됩니다.

치환은 변수를 타입으로 사상합니다. 합성이란 먼저 이전 치환을 적용한 뒤, 그 결과를 더 새로운 치환으로 다시 쓰는 것을 뜻합니다. 스킴에 치환을 적용할 때는 그 스킴의 양화된 변수를 보호해야 합니다.

#![allow(unused)]
fn main() {
impl Subst {
    fn apply(&self, ty: &Type) -> Type {
        match ty {
            Type::Var(n) => {
                self.0.get(n).map_or_else(|| ty.clone(), |t| self.apply(t))
            }
            Type::Arrow(a, b) => Type::arrow(self.apply(a), self.apply(b)),
            other => other.clone(),
        }
    }
    /// self after earlier: every earlier range is rewritten by the newer substitution.
    fn compose(&self, earlier: &Self) -> Self {
        let mut result: BTreeMap<_, _> =
            earlier.0.iter().map(|(n, t)| (*n, self.apply(t))).collect();
        for (n, t) in &self.0 {
            result.entry(*n).or_insert_with(|| t.clone());
        }
        Self(result)
    }
    fn environment(&self, env: &Env) -> Env {
        env.iter()
            .map(|(name, scheme)| {
                let mut filtered = self.clone();
                for n in &scheme.variables {
                    filtered.0.remove(n);
                }
                (
                    name.clone(),
                    Scheme {
                        variables: scheme.variables.clone(),
                        ty: filtered.apply(&scheme.ty),
                    },
                )
            })
            .collect()
    }
}
}

적용은 함수에서 알게 된 정보를 인자를 추론하는 데 쓰이는 환경으로 넘겨줍니다. 그런 다음 함수의 타입을 인자의 타입에서 새로운 결과 타입으로 가는 화살표와 단일화합니다.

#![allow(unused)]
fn main() {
Kind::App(f, a) => {
    let (s1, tf) = self.infer(env, f)?;
    let (s2, ta) = self.infer(&s1.environment(env), a)?;
    let result = self.fresh();
    let s3 = unify(
        &s2.apply(&tf),
        &Type::arrow(ta, result.clone()),
        expr,
    )?;
    Ok((s3.compose(&s2.compose(&s1)), s3.apply(&result)))
}
}

let의 경우에는 일반화하기 전에 우변의 치환을 환경에 적용합니다. 이전 환경을 기준으로 일반화하면 이미 제약된 변수가 빠져나갈 수 있습니다.

#![allow(unused)]
fn main() {
Kind::Let(n, value, body) => {
    let (s1, t) = self.infer(env, value)?;
    let mut local = s1.environment(env);
    local.insert(n.clone(), generalize(&local, s1.apply(&t)));
    let (s2, result) = self.infer(&local, body)?;
    Ok((s2.compose(&s1), result))
}
}

가변 해와 레벨

union_find.rs는 명시적인 치환 맵을 메타변수 셀로 대체합니다. 셀은 자신이 생성된 레벨과 선택적인 해를 기록합니다. 해가 정해진 변수를 따라가면 그 경로가 압축되므로, 이후의 방문에서는 해결된 타입에 더 빨리 도달할 수 있습니다.

#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Meta {
    level: u32,
    solution: Option<Type>,
}

#[derive(Default)]
struct Infer {
    level: u32,
    metas: Vec<Meta>,
}

impl Infer {
    fn fresh(&mut self) -> Type {
        let n = self.metas.len() as u32;
        self.metas.push(Meta {
            level: self.level,
            solution: None,
        });
        Type::Var(n)
    }
    fn zonk(&mut self, ty: &Type) -> Type {
        match ty {
            Type::Var(n) => {
                match self.metas[*n as usize].solution.clone() {
                    None => ty.clone(),
                    Some(t) => {
                        let t = self.zonk(&t);
                        self.metas[*n as usize].solution = Some(t.clone());
                        t
                    }
                }
            }
            Type::Arrow(a, b) => Type::arrow(self.zonk(a), self.zonk(b)),
            other => other.clone(),
        }
    }
}

let의 우변을 추론할 때는 더 깊은 레벨로 들어갑니다. 더 오래된 메타변수에 해를 설치하기 전에, 출현 검사(occurs check) 순회는 그 해에서 도달할 수 있는 모든 메타변수의 레벨을 그 오래된 레벨로 낮춥니다. 이 변수들은 let 안에서 할당되었더라도 이제 바깥 스코프에 속합니다.

#![allow(unused)]
fn main() {
/// One traversal rejects cycles and prevents younger variables escaping their scope.
fn occurs_and_lower(
    &mut self,
    n: u32,
    level: u32,
    ty: &Type,
    expr: &Expr,
) -> Result<(), Diagnostic> {
    match self.zonk(ty) {
        Type::Var(m) if m == n => Err(types::occurs(expr)),
        Type::Var(m) => {
            self.metas[m as usize].level =
                self.metas[m as usize].level.min(level);
            Ok(())
        }
        Type::Arrow(a, b) => {
            self.occurs_and_lower(n, level, &a, expr)?;
            self.occurs_and_lower(n, level, &b, expr)
        }
        _ => Ok(()),
    }
}
fn unify(
    &mut self,
    a: &Type,
    b: &Type,
    expr: &Expr,
) -> Result<(), Diagnostic> {
    let (a, b) = (self.zonk(a), self.zonk(b));
    match (&a, &b) {
        _ if a == b => Ok(()),
        (Type::Var(n), t) | (t, Type::Var(n)) => {
            self.occurs_and_lower(
                *n,
                self.metas[*n as usize].level,
                t,
                expr,
            )?;
            self.metas[*n as usize].solution = Some(t.clone());
            Ok(())
        }
        (Type::Arrow(a, b), Type::Arrow(c, d)) => {
            self.unify(a, c, expr)?;
            self.unify(b, d, expr)
        }
        _ => Err(types::mismatch(expr, &a, &b)),
    }
}
}

감싸는 레벨로 돌아온 뒤, 일반화는 레벨이 여전히 더 깊은 미해결 변수만 선택합니다. 일반화는 환경을 훑지 않습니다. 스킴이 나타날 때마다 그 스킴의 양화된 변수는 새 셀로 대체됩니다. 양화되지 않은 변수는 자신의 정체성을 유지하므로 제약도 그대로 유지합니다.

#![allow(unused)]
fn main() {
Kind::Let(n, value, body) => {
    self.level += 1;
    let result = self.infer(env, value);
    self.level -= 1;
    let scheme = self.generalize(result?);
    env.push((n.clone(), scheme));
    let result = self.infer(env, body);
    env.pop();
    result
}
}

\x -> let y = \z -> x z in ...를 시도해 보십시오. y의 타입은 x에 의존하므로, 우리는 두 호출이 그 타입으로 서로 무관한 타입을 고르게 해서는 안 됩니다. y를 정수와 불리언 양쪽으로 호출하면 실패해야 합니다. 이는 레벨을 낮추는 코드 옆에 두고 보기 좋은 예제입니다. 이 예제는 우리가 지나치게 일반화하면 정확히 무엇이 잘못되는지 보여 줍니다. 우리는 거부되어야 하는 프로그램을 포함하여 이와 같은 예제들로 두 추론기를 비교합니다.

실행해 보기

$ cd crates/ch06_poly
$ cargo run -q -- run tests/programs/poly-id.fun
True
$ cargo run -q -- dump --pass=types tests/programs/missing-annotation.fun
a -> a
$ cargo run -q -- dump --pass=types-w tests/programs/missing-annotation.fun
a -> a
$ cargo run -q -- repl
Poly: :quit, :type EXPR, :parse EXPR, :trace EXPR.
poly> :type \f -> \x -> f x
(a -> b) -> a -> b
poly> :quit

types와 일반적인 검사 경로는 레벨 기반 추론기를 사용합니다. types-w는 알고리즘 W가 독립적으로 낸 결과를 선택합니다. 표시된 주 타입의 자유 변수는 암묵적으로 양화된 것입니다. 평가는 여전히 기본적으로 엄격한 CEK 기계를 사용합니다. 추론 방식을 바꾸어도 런타임 값은 바뀌지 않습니다.

연습 문제

  1. 쉬움. 먼저 새 변수들을 적어 둔 다음 \x -> \y -> x의 타입을 추론하십시오.
  2. 보통. let id = \x -> x in id id에 대한 치환을 추적하십시오.
  3. 보통. 처음에 본 id 예제는 성공하는데 \f -> let a = f 1 in f True는 왜 실패하는지 설명하십시오.
  4. 어려움. 바깥 람다 매개변수를 포획하는 안쪽 함수에 대해 메타변수 셀과 레벨을 그려 보십시오. 레벨을 낮추는 단일화를 찾아내십시오.

더 읽을거리

Damas와 Milner는 주 타입 스킴을 확립합니다(Damas and Milner 1982). Kiselyov는 Rémy의 연구(Rémy 1992)를 바탕으로 OCaml 구현을 통해 레벨을 설명합니다(Kiselyov 2013). Pierce는 타입 판단과 유도에 대한 더 폭넓은 입문을 제공합니다(Pierce 2002).

이 아이디어들을 다른 경로로 살펴보려면 타입 시스템만 다룬 제 다른 글을 참고하십시오.

7장: Fun의 설계

새 파일

  • src/session.rs
  • src/core.rs
  • src/typecheck.rs
  • src/elaborate.rs
  • src/diagnostics/codes.rs
  • src/diagnostics/mod.rs
  • src/parse.rs
  • src/source.rs

변경된 파일

  • src/eval.rs
  • src/ast.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/names.rs
  • src/types.rs

동기

우리는 파서를 만들었고, 함수가 어떻게 실행되는지 살펴보았으며, 컴파일러에게 타입을 추론하도록 가르쳤습니다. 이제 이 조각들을 하나로 모을 수 있습니다. 이 장부터 우리는 최종적으로 네이티브 코드로 컴파일할 언어인 Fun을 만들어 나갈 것입니다. 각 장은 실행 가능한 스냅숏을 유지하므로, 어떤 아이디어가 어디서 시작되었는지 보고 싶을 때 언제든 더 단순한 버전으로 돌아갈 수 있습니다.

여기서 하는 작업 중 일부는 흥미로운 부분을 즐겁게 만들 수 있도록 해 주는 평범한 엔지니어링입니다. 타입 검사기가 실수를 발견하면, 그 실수가 소스의 어디에 있는지 가리킬 수 있어야 합니다. 한 패스가 실패하면, 다음 패스는 그 패스가 끝내지 못한 출력을 이해하려 들어서는 안 됩니다. 우리는 이름, 소스 위치, 오류에 공통의 거처를 마련해 준 다음, 패스들을 연결하여 계속 확장해 나갈 수 있는 컴파일러를 만들 것입니다.

현재의 언어

프로그램은 선언을 담은 명시적인 중괄호 블록입니다. 선언은 세미콜론으로 구분합니다. 끝에 붙는 세미콜론은 생략해도 됩니다. 각 선언은 자기 자신과 앞선 선언을 참조할 수 있습니다. 전방 참조와 상호 재귀는 9장에서 다룹니다.

{
  twice f x = f (f x);
  sum n = if n == 0 then 0 else n + sum (n - 1);
  main = twice (\x -> x + 1) (sum 10);
}

지역 let도 중괄호를 사용하며, 이 스냅숏에서는 바인딩을 하나만 둡니다. 재귀 바인딩에는 rec 키워드가 필요하지 않습니다. 자기 참조가 있으면 재귀로 식별하며, 이때 우변은 람다여야 합니다. let { x = x; } in x는 E0120으로 거부됩니다.

Char는 유니코드 스칼라 값을 담고, String은 원시 UTF-8 문자열입니다. 문자열은 단형 ++ 연산자로 연결합니다. 정수는 16진수 표기법과 밑줄을 허용합니다. 줄 주석과 중첩 블록 주석을 모두 지원합니다.

{ {- outer {- nested -} -} main = "Hello, " ++ "λ\n"; }

run은 여전히 main의 값을 출력하며, 문자열은 따옴표와 이스케이프를 포함해 출력합니다. 아직 IO를 실행하지는 않습니다. 정수 div와 mod는 내림(floor) 의미론을 따르는 커리된 내장 함수입니다. div 7 (-3)은 -3이고, mod 7 (-3)은 -2입니다. 이에 따라 예전 계산기의 버림 나눗셈 픽스처도 바뀝니다.

이론

소스 프로그램과 실행 가능한 중간 언어는 서로 다른 목적을 가집니다. 소스 트리는 철자, 구문, 소스 스팬을 보존합니다. Core는 평가에 필요한 연산만 남깁니다. 하나를 다른 하나로 정교화(elaboration)하면 언어의 편의 기능이 명시적으로 드러납니다. 나중에 우리는 Core에 타입을 추가하여 별도의 검사기가 각 변환을 검증할 수 있게 합니다 (Pierce 2002).

엄격성은 여전히 의미론상의 요구 사항입니다. 함수는 인자보다 먼저 평가되고, 연산자의 피연산자는 왼쪽에서 오른쪽으로 평가되며, 조건식은 하나의 분기만 선택합니다. 최상위 값은 main의 최종 값이 선택되기 전에 한 번씩 초기화되며, 이 스냅숏에서는 선언 순서를 따릅니다. 사용되지 않는 초기화식이라도 실패하면 프로그램이 멈춥니다. 최상위 함수 본문은 적용될 때까지 평가되지 않습니다.

현재의 타입 시스템은 여전히 힌들리–밀너이며, 6장의 레벨 기반 검사기를 사용합니다 [Damas & Milner (1982); Kiselyov (2013)]. 재귀 바인딩 제약은 값이 자기 자신의 초기화가 끝나기 전에 생성되는 일을 막습니다. 대신 재귀 함수는 진입할 때 자기 자신의 클로저를 참조합니다. 여기서 일반적인 이름 스코프는 여전히 철자 기반 맵을 사용합니다. 고유한 바인딩 식별자는 9장에서 실제로 쓰이기 시작합니다.

구현

소스와 세션 상태

source.rs는 각 파일에 고유한 식별자를 부여하고 줄 시작 위치 테이블을 저장합니다. 스팬은 렉서와 마찬가지로 반열린 바이트 범위이며, 화면에 표시되는 열 번호는 유니코드 스칼라 값 단위로 셉니다. 파일 식별자가 있으므로 서로 다른 파일에서 오프셋이 같아도 혼동되지 않습니다.

#![allow(unused)]
fn main() {
/// Index of a source file in a session's source map.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FileId(pub u32);

/// Half-open byte offsets within one source file.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Span {
    pub file: FileId,
    pub start: u32,
    pub end: u32,
}

impl Span {
    pub fn new(file: FileId, start: usize, end: usize) -> Self {
        Self {
            file,
            start: start.try_into().expect("source exceeds 4 GiB"),
            end: end.try_into().expect("source exceeds 4 GiB"),
        }
    }
    pub fn range(self) -> Range<usize> {
        self.start as usize..self.end as usize
    }
    pub fn through(self, other: Self) -> Self {
        assert_eq!(self.file, other.file);
        Self {
            end: other.end,
            ..self
        }
    }
}

/// Original text and line starts; columns count Unicode scalar values.
pub struct SourceFile {
    pub name: String,
    pub text: String,
    lines: Vec<usize>,
}

#[derive(Default)]
/// Owns every source used by this compilation, including future Prelude input.
pub struct SourceMap {
    pub files: Vec<SourceFile>,
}

impl SourceMap {
    pub fn add(&mut self, name: impl Into<String>, text: String) -> FileId {
        assert!(u32::try_from(text.len()).is_ok(), "source exceeds 4 GiB");
        let mut lines = vec![0];
        lines.extend(text.match_indices('\n').map(|(i, _)| i + 1));
        let id = FileId(
            self.files.len().try_into().expect("too many source files"),
        );
        self.files.push(SourceFile {
            name: name.into(),
            text,
            lines,
        });
        id
    }
    pub fn get(&self, file: FileId) -> &SourceFile {
        &self.files[file.0 as usize]
    }
    pub fn line_col(&self, span: Span) -> (usize, usize) {
        let file = self.get(span.file);
        let offset = span.start as usize;
        let line = file.lines.partition_point(|start| *start <= offset) - 1;
        (
            line + 1,
            file.text[file.lines[line]..offset].chars().count() + 1,
        )
    }
}
}

names.rs는 직접 작성한 인터너와 새 이름 공급기를 제공합니다. 철자가 같으면 하나의 Symbol을 공유합니다. 공급기를 거듭 호출하면 매번 서로 다른 Name 식별자가 만들어집니다. 이 정의들은 렉시컬 스코프를 서둘러 바꾸지 않으면서, 이후에 만들 리네이머가 사용할 인터페이스를 마련합니다.

세션은 이 기능들과 함께 옵션과 진단 싱크를 소유합니다:

#![allow(unused)]
fn main() {
#[derive(Default)]
/// No pass needs a global interner, source map, or diagnostic sink.
pub struct Session {
    pub sources: SourceMap,
    pub interner: Interner,
    pub supply: Supply,
    pub options: Options,
    pub diagnostics: Vec<Diagnostic>,
}

impl Session {
    pub fn emit(&mut self, diagnostic: Diagnostic) {
        self.diagnostics.push(diagnostic);
    }
    pub fn finish<T>(&self, value: T) -> Result<T, Stop> {
        if self
            .diagnostics
            .iter()
            .any(|d| d.severity == Severity::Error)
        {
            Err(Stop)
        } else {
            Ok(value)
        }
    }
}
}

각 패스는 &mut Session을 받아 Result<Output, Stop>을 반환합니다. Stop은 진단의 사본을 따로 들고 있지 않습니다. 드라이버는 누적된 구조화된 오류를 출력하고 다음 패스를 호출하기 전에 종료합니다. 렉싱과 중복 정의 검사는 오류를 모아 둡니다. 추론은 타입 오류 때문에 의미 있는 결과를 낼 수 없는 지점에서 멈춥니다.

diagnostics/codes.rs는 오류와 경고를 위한 안정적인 레지스트리로, 앞으로 추가될 기능을 위해 예약해 둔 코드도 담고 있습니다. 진단은 심각도, 메시지, 소스 레이블, 노트를 가집니다. 이를 출력하는 것은 Ariadne입니다. stderr의 처음 두 줄은 여전히 코퍼스 실행기가 사용하는 안정적인 코드와 주 소스 위치입니다.

표면 검사와 Core

lexer.rs는 logos를 사용하며, 중첩 주석과 리터럴 이스케이프를 위한 콜백도 포함합니다. 렉서의 예약어 테이블과 연산자 테이블은 cargo xtask syntax를 통해 HTML 및 PDF용 구문 정의도 생성합니다. 책 빌드는 이 출력물이 어긋나지 않았는지 검사합니다.

parser.lalrpop은 스팬이 붙은 표현식과 최상위 바인딩을 만듭니다. 등식의 매개변수는 중첩된 람다가 되며, 중복된 매개변수는 거부됩니다. 검사기는 선언을 소스 순서대로 처리합니다. 자기 자신에 의존하는 바인딩의 경우, 검사기는 람다를 검사하는 동안 임시 단형 타입을 도입하고, 결과를 단일화한 다음, 앞 장에서처럼 일반화합니다.

elaborate.rs는 검사를 마친 트리를 별도의 Core 표현으로 낮춥니다:

#![allow(unused)]
fn main() {
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Int(i64),
    Bool(bool),
    Char(char),
    String(String),
}

#[derive(Clone, Debug)]
/// The initial lambda Core; types, data constructors, and compiled cases arrive later.
pub enum Expr {
    Var(String),
    Lit(Lit),
    Lam(String, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(Bind, Box<Expr>),
    If(Box<Expr>, Box<Expr>, Box<Expr>),
    Prim(Op, Vec<Expr>, Span),
}

#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: String,
    pub rhs: Box<Expr>,
    pub recursive: bool,
}

#[derive(Clone, Debug)]
/// Globals retain source order in this chapter; SCC ordering arrives in Chapter 9.
pub struct Program {
    pub bindings: Vec<Bind>,
    pub main: String,
}
}

연산자는 포화된 프리미티브 호출이 됩니다. 부정은 0에서 빼는 연산이 됩니다. 초기 내장 환경은 div와 mod에 대한 커리된 래퍼를 제공합니다. --no-prelude는 격리된 테스트를 위해 이 래퍼들을 제거합니다. 소스 Prelude는 데이터 타입과 함께 도입됩니다. prim_으로 시작하는 소스 이름은 앞으로 도입될 그 Prelude 경로 밖에서는 E0111로 거부됩니다.

--lint는 현재 Core의 스코프, 프리미티브 포화, 재귀 람다 불변식을 검사합니다. 이것은 구조적 린트일 뿐, 타입 검사기가 아닙니다. 10장에서 이를 완전한 System F 검사로 대체할 것입니다. 골든 하네스는 매 실행마다 이를 활성화합니다.

참조 평가

eval.rs는 Core를 직접 해석합니다. 재귀 클로저는 순환하는 Rust 환경을 만들지 않고 자기 자신의 이름을 저장합니다. 적용은 그 클로저를 매개변수보다 먼저 캡처된 환경에 삽입합니다:

#![allow(unused)]
fn main() {
Expr::App(function, argument) => {
    let function = evaluate(function, env, fuel)?;
    let argument = evaluate(argument, env, fuel)?;
    if let Value::Closure {
        parameter,
        body,
        env,
        recursive,
    } = &function
    {
        let mut local = (**env).clone();
        if let Some(name) = recursive {
            local.push((name.clone(), function.clone()));
        }
        local.push((parameter.clone(), argument));
        evaluate(body, &local, fuel)
    } else {
        Err("internal error: non-function application".into())
    }
}
}

프로그램 실행기는 모든 최상위 바인딩을 순서대로 초기화하고 그 값들을 환경에 보관합니다. 정수는 산술 연산 후 63비트로 정규화되며, 나눗셈은 내림 몫과 그에 맞는 나머지를 계산하기 전에 0인지 검사합니다. 문자와 문자열 출력은 소스 리터럴과 같은 이스케이프 규칙을 공유합니다.

이 참조 평가기는 512 MiB 스레드 스택 위에서 동작하는 재귀적 트리 워커입니다. 실험을 위한 연료 제한은 있지만, 상수 스택 보장은 없습니다. 5장의 명시적 기계는 18장에서 ANF 오라클로 다시 등장합니다. Core의 참조 인터프리터를 직접적인 형태로 유지하면 앞으로 나올 정교화 패스들을 살펴보기가 더 쉬워집니다.

실행해 보기

$ cd crates/ch07_fun
$ cargo run -q -- run tests/programs/fun-top.fun
42
$ cargo run -q -- dump --pass=types tests/programs/fun-string.fun
main : String
$ cargo run -q -- dump --pass=core --no-prelude tests/programs/fun-char.fun
val main = 'λ'
$ cargo run -q -- repl
Fun: expressions, :type EXPR, :core EXPR, :quit.
fun> :type \x -> x
main : a -> a
fun> :quit

같은 작은 프로그램에 대해 tokens, parse, types, core 덤프를 시도해 보십시오. 각 덤프는 여정의 서로 다른 단계를 보여 줍니다. dump --pass=...나 공통 옵션인 --dump=...를 사용할 수 있습니다. 현재 REPL은 개별 표현식을 평가합니다. 영속적인 정의는 JIT와 함께 도입될 것입니다.

연습 문제

  1. 쉬움. --dump=tokens를 사용해 중첩 주석과 연산자 -->를 비교해 보십시오.
  2. 보통. 0으로 나누는, 사용되지 않는 최상위 초기화식을 추가해 보십시오. main이 왜 결코 값을 내지 못하는지 설명해 보십시오.
  3. 보통. 지역 재귀 함수의 Core를 살펴보고, 자기 참조가 어디에서 바인딩되는지 설명해 보십시오.
  4. 어려움. 소스 파싱은 통과하지만 잘못된 컴파일러 변환에서만 만들어질 수 있는, 유효하지 않은 Core 프로그램을 구성하십시오. 린트 테스트를 확장하여 이 프로그램을 거부하도록 하십시오.

더 읽을거리

Haskell Report는 Fun이 점진적으로 채택하는 어휘 및 스코프 규칙의 기준을 제공합니다 (Simon Marlow 2010). Pierce는 타입이 있는 중간 언어의 관점을 전개합니다 (Pierce 2002).

8장: 레이아웃과 확장된 파서

새 파일

  • src/layout.rs
  • src/fixity.rs
  • src/format.rs

변경된 파일

  • src/parse.rs
  • src/typecheck.rs
  • src/ast.rs
  • src/elaborate.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/types.rs

동기

중괄호는 우리가 처음 시작할 때는 유용했습니다. 이제 와서는 대부분 들여쓰기만 봐도 이미 알 수 있는 구조를 위해 구두점을 일일이 타입해 넣게 만들 뿐입니다. Fun을 조금 더 작성하기 편한 언어로 만들어 봅시다.

우리는 레이아웃, where를 사용한 지역 보조 함수, 타입 시그니처, 사용자 정의 연산자를 추가할 것입니다. 이 작업의 상당 부분은 평가 이전에 이루어질 수 있습니다. 들여쓰기는 구두점이 되고, 섹션은 람다가 되며, 연산자 연쇄는 선언된 결합성을 알게 되는 시점에 묶음이 정해집니다. 이것은 만족스러운 컴파일러 기법입니다. 우리는 평가기를 작게 유지하면서도 소스 언어를 더 표현력 있게 만들 수 있습니다.

현재의 언어

들여쓰기가 경계를 제공하는 경우 중괄호와 세미콜론은 선택 사항이 됩니다. where 블록은 해당 등식에 속합니다. 등식의 매개변수는 그 블록 안에서도 계속 보입니다. 여러 지역 선언이 하나의 let 블록을 공유할 수 있습니다.

addTwo x = x + y
  where
    y = 2

main = let result = addTwo 40 in result

시그니처는 같은 블록 안에서 해당 바인딩의 앞이나 뒤에 올 수 있습니다. 타입 변수는 암묵적으로 한정됩니다. 시그니처는 추론된 타입을 특수화할 수는 있지만, 구현을 실제보다 더 다형적으로 만들 수는 없습니다.

id :: a -> a
id x = x

main = let { n :: Int; n = id 42; } in n

연산자는 전위 형식이나 중위 형식으로 정의할 수 있습니다. 연산자의 결합성은 결합성 선언보다 앞선 위치에서도 파일 전체에 적용됩니다. 선언되지 않은 연산자는 우선순위 9에서 왼쪽으로 결합합니다. 이름 있는 함수는 백틱 안에 넣으면 연산자가 됩니다.

infixr 5 %%

a %% b = a * 10 + b

main = 1 %% 2 %% 3

이 프로그램은 1 %% (2 %% 3)을 의미하므로 33을 출력합니다. 섹션은 인자 하나를 채워 줍니다. (40 +)와 (+ 2)는 함수입니다. 반면 (- 2)는 부정(negation)을 의미합니다. 단항 마이너스는 우선순위가 6이므로 -2 * 3 + 4는 (-(2 * 3)) + 4를 의미합니다. 우선순위가 6 이상인 연산자 뒤에 부정을 쓸 때는 괄호로 감싸십시오.

이론

공백을 신비로운 것으로 여기지 않게 되면 레이아웃은 이해하기가 더 쉬워집니다. 우리는 들여쓰기를 우리 파서가 이미 알고 있는 중괄호와 세미콜론으로 변환할 것입니다. Haskell Report가 이 변환을 설명합니다 (Simon Marlow 2010). 어댑터를 살펴보기 전에 직접 손으로 해 봅시다.

보이지 않는 구분자 보기

들여쓰기를 구두점의 약식 표기라고 생각하십시오. 블록의 첫 번째 선언이 그 블록의 왼쪽 여백을 정합니다. 그 여백에서 시작하는 또 다른 선언은 새 항목을 시작합니다. 바깥쪽 여백으로 되돌아가면 블록이 끝납니다.

scale x = result
  where
    doubled = x + x
    result = doubled * 3

main = let n = 7 in scale n

파싱하기 전에 레이아웃은 생략된 구두점을 명시적으로 드러냅니다. 여기서 모든 중괄호와 세미콜론은 레이아웃 패스가 삽입한 것입니다.

{ scale x = result
    where { doubled = x + x
          ; result = doubled * 3
          }
; main = let { n = 7 } in scale n
}

파일 전체에는 바깥쪽 중괄호 한 쌍이 붙습니다. doubled가 where 블록의 들여쓰기를 정하고, 같은 열에 있는 result 앞에는 구분자가 붙습니다. main은 더 왼쪽에 있으므로 레이아웃은 where 블록을 닫고 다음 최상위 선언을 구분합니다. 같은 줄의 in은 내어쓰기가 없었는데도 let 블록을 닫습니다. 마지막 중괄호는 입력이 끝나는 지점에서 파일을 닫습니다. 중괄호를 직접 써도 같은 묶음이 만들어집니다.

이후의 패턴 장과 IO 장에서는 of와 do 뒤에 같은 메커니즘을 사용합니다.

main = do
  case flag of
    True -> putStrLn "yes"
    False -> putStrLn "no"
  putStrLn "done"

이를 명시적으로 쓰면 다음과 같습니다.

{ main = do
    { case flag of
        { True -> putStrLn "yes"
        ; False -> putStrLn "no"
        }
    ; putStrLn "done"
    }
}

마지막 동작 앞의 내어쓰기는 of 블록만 닫습니다. 그 동작은 여전히 do 안에 있습니다. 이 구문들은 나중에 등장하지만, 그 들여쓰기를 변환하는 규칙은 이미 같습니다. 삽입된 구두점과 소스 문자를 구별하는 스팬을 포함하여 실제 토큰을 보려면 --dump=layout을 사용하십시오.

결합성 해석(fixity resolution)은 적용을 구성하기 전에 인접한 연산자들을 비교합니다. 우선순위가 더 높은 연산자가 먼저 결합합니다. 우선순위가 같으면 결합 방향이 서로 호환되어야 합니다. 좌결합 연산자 두 개는 왼쪽부터, 우결합 연산자 두 개는 오른쪽부터 축약됩니다. 비결합 연산자나, 같은 우선순위에서 결합 방향이 섞인 연산자에는 괄호가 필요합니다 (Simon Marlow 2010).

시그니처는 약속을 합니다. a -> a라고 쓰면, 그 함수가 호출자가 고르는 어떤 a에 대해서도 동작한다는 뜻입니다. 검사기가 몰래 a = Int를 골라서 그 약속을 참으로 만들어서는 안 됩니다.

우리는 시그니처의 양화된 변수를 스콜렘(skolem)이라 불리는 경직된 식별자로 바꿔서 이를 강제합니다. 추론 변수는 풀 수 있지만, 스콜렘은 임의의 타입을 나타내며 계속 임의의 타입으로 남아야 합니다. 추론된 스킴을 새로운 유연한(flexible) 변수들로 인스턴스화하고, 이를 경직된 시그니처와 단일화해 봅니다. 또한 어떤 스콜렘도 바깥 스코프로 탈출하지 않는지 검사합니다. 이 검사들을 함께 거치면 추론된 타입이 그 약속을 뒷받침하는지 알 수 있습니다 [Damas & Milner (1982); Pierce (2002)].

구현

레이아웃과 트리비아

logos 렉서는 주석 토큰을 원래 텍스트와 함께 내보냅니다. TriviaLexer는 이를 별도 테이블에 기록하고 의미 있는 토큰만 노출합니다. 그런 다음 LayoutLexer가 들여쓰기 삽입을 수행합니다. 어댑터는 명시적인 중괄호와 괄호를 묶고 그 안의 레이아웃을 재귀적으로 처리하므로, 괄호로 감싼 let도 자신만의 블록을 얻습니다.

#![allow(unused)]
fn main() {
pub fn layout(
    sess: &mut Session,
    file: FileId,
) -> Result<Vec<Spanned>, Stop> {
    let tokens = lexer::lex(sess, file)?;
    let source = &sess.sources.get(file).text;
    let eager = !matches!(tokens.first(), Some((_, Token::Open, _)));
    // offsides measures byte columns with fixed-width tabs. Give it a shadow
    // source whose byte positions are visual columns, then restore original spans.
    let (shadow, forward, backward) = visual_source(source);
    let tokens = tokens
        .into_iter()
        .map(|(lo, t, hi)| (forward[lo], t, forward[hi]))
        .collect::<Vec<_>>();
    match region(&shadow, &tokens, eager)
        .map(|tokens| {
            tokens
                .into_iter()
                .map(|(lo, t, hi)| (backward[lo], t, backward[hi]))
                .collect()
        })
        .map_err(|(lo, hi, message)| (backward[lo], backward[hi], message))
    {
        Ok(tokens) => Ok(tokens),
        Err((lo, hi, message)) => {
            sess.emit(Diagnostic::error(
                ErrorCode::Parse,
                message,
                Span::new(file, lo, hi),
            ));
            Err(Stop)
        }
    }
}
}

이 라이브러리는 let/in 클로저 연산을 노출하지 않습니다. 어댑터는 어떤 가상 프레임이 in에서 이미 닫혔는지 추적하여, 그 프레임들의 이후 구분자와 닫는 중괄호를 억제합니다. 또한 빈 블록과, 명시적 세미콜론 옆의 불필요한 가상 구분자도 처리합니다. 토큰은 원래의 바이트 오프셋을 유지합니다. 삽입된 구두점은 너비가 0인 스팬을 가집니다.

이제 of와 do는 어휘 레이아웃 테스트에 참여합니다. 이들의 언어 구문은 여전히 각각 11장과 15장을 기다립니다. 컴파일러는 그 표현식들을 미리 받아들이지 않습니다.

연산자 연쇄

문법은 첫 번째 피연산자와 그 뒤에 이어지는 연산자/피연산자 쌍들을 유지합니다. 괄호는 해석기가 그 경계를 확정할 때까지 명시적으로 남아 있습니다. 스택이 이 체인들을 해석합니다. 따로 구별된 단항 항목이 부정(negation)에 우선순위를 부여합니다.

#![allow(unused)]
fn main() {
fn chain(
    &mut self,
    first: Expr,
    pairs: Vec<(String, Expr)>,
) -> Result<Expr, Diagnostic> {
    let mut values = Vec::new();
    let mut operators: Vec<Option<String>> = Vec::new();
    self.operand(first, &mut values, &mut operators)?;
    for (op, value) in pairs {
        let next = self.fix(&op);
        while let Some(previous) = operators.last() {
            let prior =
                previous.as_ref().map(|p| self.fix(p)).unwrap_or(Fix {
                    precedence: 6,
                    assoc: Assoc::Left,
                });
            if prior.precedence < next.precedence {
                break;
            }
            if prior.precedence == next.precedence && previous.is_some()
            {
                if prior.assoc != next.assoc
                    || prior.assoc == Assoc::None
                {
                    return Err(self.error(value.span,"operators with conflicting fixities need parentheses"));
                }
                if prior.assoc == Assoc::Right {
                    break;
                }
            }
            let prior = operators.pop().unwrap();
            self.reduce(&mut values, prior);
        }
        operators.push(Some(op));
        self.operand(value, &mut values, &mut operators)?;
    }
    while let Some(op) = operators.pop() {
        self.reduce(&mut values, op);
    }
    Ok(values.pop().unwrap())
}
}

섹션은 소스 식별자로는 쓸 수 없는 생성된 이름을 가진 람다가 됩니다. 현재의 기본 연산자는 가려지지 않았을 때 직접적인 산술 노드로 남습니다. 사용자 연산자는 평범한 커링된 적용이 됩니다. 따라서 Core에는 새로운 식 형태가 필요하지 않습니다.

시그니처 검사

선언은 시그니처가 바인딩에 붙기 전에 수집됩니다. 검사기는 먼저 바인딩을 추론하고 일반화한 다음, 시험용 저장소에서 그 시그니처를 검사합니다. 검사가 성공하면 이후 출현들이 사용하는 현재 스킴을 특수화합니다. 재귀 호출은 추론 중에 단형으로 남습니다. 시그니처가 이끄는 다형 재귀는 이후의 시그니처 검사기와 함께 도입됩니다.

#![allow(unused)]
fn main() {
fn check_signature(
    &mut self,
    inferred: &Scheme,
    declared: &Scheme,
    expr: &Expr,
) -> Result<Scheme, Diagnostic> {
    fn rigid(ty: &Type) -> Type {
        match ty {
            Type::Var(n) => Type::Skolem(*n),
            Type::Arrow(a, b) => Type::arrow(rigid(a), rigid(b)),
            t => t.clone(),
        }
    }
    fn has_skolem(ty: &Type) -> bool {
        match ty {
            Type::Skolem(_) => true,
            Type::Arrow(a, b) => has_skolem(a) || has_skolem(b),
            _ => false,
        }
    }
    let mut trial = self.clone();
    let actual = trial.instantiate(inferred);
    let expected = rigid(&declared.ty);
    let invalid = trial.unify(&actual, &expected, expr).is_err()
        || (0..self.metas.len()).any(|n| {
            self.metas[n].level <= self.level
                && has_skolem(&trial.zonk(&Type::Var(n as u32)))
        });
    if invalid {
        return Err(Diagnostic::error(
            ErrorCode::Signature,
            format!(
                "signature {} is not an instance of inferred type {}",
                declared.ty,
                inferred.ty.canonical()
            ),
            expr.span,
        ));
    }
    // Specialization can constrain variables captured from an outer scope.
    self.level += 1;
    let actual = self.instantiate(inferred);
    let expected = self.instantiate(declared);
    let result = self.unify(&actual, &expected, expr);
    self.level -= 1;
    result?;
    Ok(self.generalize(expected))
}
}

서식화

포매터는 marginalia의 문서 대수와 부착된 주석 맵을 사용합니다. 포매터의 정규 출력은 명시적인 블록을 사용하고 소스의 괄호를 유지합니다. 주석은 토큰 경계에서 별도의 앞선 줄로 옮겨지며, 그 내용과 순서는 보존됩니다. 이렇게 하면 줄 주석 안에 구두점을 삽입하는 일을 피할 수 있습니다.

#![allow(unused)]
fn main() {
pub fn format(sess: &mut Session, file: FileId) -> Result<String, Stop> {
    let parsed = parse::parse(sess, file)?;
    crate::fixity::resolve(sess, parsed)?;
    let (semantic, trivia) = lexer::trivia(sess, file)?;
    let tokens = layout::layout(sess, file)?;
    let source = &sess.sources.get(file).text;
    let mut comments = attach(
        source,
        &trivia,
        semantic.iter().map(|(lo, _, hi)| span(*lo, *hi)),
        AttachOptions::default(),
    );
    // Tokens are smaller anchors than statements. Put their comments on
    // separate leading lines so a trailing line comment cannot swallow a
    // newly inserted separator or a following `where`.
    for (lo, _, hi) in &semantic {
        let entry = comments.entry(span(*lo, *hi));
        entry.leading.append(&mut entry.trailing);
    }
    let mut cursor = 0;
    let doc = document(source, &tokens, &mut cursor, false);
    let rendered = pretty::render(&doc, &comments, RenderOpts::default());
    Ok(format!("{}\n", rendered.trim_end()))
}
}

테스트는 단순히 평가 결과를 비교하는 대신, 스팬을 제거한 뒤 파싱된 트리를 비교합니다. 구문적으로 올바른 모든 코퍼스 프로그램은 왕복 변환되어야 하며, 출력을 다시 서식화하면 정확히 같은 바이트가 나와야 합니다. 전용 사례들은 중첩된 블록 주석과 where 옆의 주석을 유지하는지 확인합니다.

실행해 보기

$ cd crates/ch08_layout
$ cargo run -q -- run tests/programs/layout/right-fixity.fun
33
$ cargo run -q -- dump --pass=fixity tests/programs/layout/negation.fun
{
  main = -(2 * 3) + 4;
}
$ cargo run -q -- fmt example.fun
$ cargo run -q -- fmt example.fun --check

세 가지 덤프는 유용한 짧은 둘러보기가 됩니다. --dump=layout은 삽입된 구두점을 드러내고, --dump=parse는 묶기 전의 연산자 연쇄를 보여 주며, --dump=fixity는 선택된 묶음을 보여 줍니다. 들여쓰기나 결합성 선언 하나만 바꿔 보고 어느 단계가 가장 먼저 달라지는지 관찰해 보십시오.

연습 문제

  1. 입문. 결합성이 서로 다른 세 연산자의 묶음을 예측한 다음, fixity 덤프를 여러분의 답과 비교해 보십시오.
  2. 중급. (1 + 2 *)가 거부되는 이유를 설명하고 괄호로 고쳐 보십시오.
  3. 중급. 연산자와 그 피연산자 사이에 중첩된 주석이 있는 포매터 테스트를 추가하십시오. 트리 동등성과 주석 보존을 모두 확인하십시오.
  4. 고급. 스콜렘이 포획된 람다 매개변수를 통해 탈출하게 되는 지역 시그니처를 구성해 보십시오. 이를 거부하는 시험용 저장소 검사를 추적해 보십시오.
  5. 고급. 제한된 in 규칙을 Report의 일반적인 parse-error(t) 규칙과 비교해 보십시오. 후자가 더 많은 일을 하게 되는 구문을 찾아보십시오.

더 읽을거리

Report는 레이아웃과 결합성 해석을 정확하게 규정합니다 (Simon Marlow 2010). Damas와 Milner는 시그니처 검사에 쓰이는 주요 타입(principal type) 성질을 정립합니다 (Damas and Milner 1982). Pierce는 다형 타입과 그 인스턴스 사이의 구별을 더 넓은 타입 이론의 틀에서 전개합니다 (Pierce 2002).

9장: 리네이머

새 파일

  • src/rename.rs
  • src/scc.rs
  • src/renamed.rs

변경된 파일

  • src/names.rs
  • src/typecheck.rs
  • src/ast.rs
  • src/core.rs
  • src/elaborate.rs
  • src/eval.rs
  • src/fixity.rs
  • src/lib.rs
  • src/main.rs
  • src/parse.rs
  • src/types.rs

동기

두 변수가 모두 x라는 이름을 가지면서도 전혀 다른 것을 의미할 수 있습니다. 작은 식에서는 이를 따라가기가 그리 어렵지 않습니다. 하지만 컴파일러가 식을 이리저리 옮기기 시작하면 사정이 덜 편해집니다. 우리는 각 출현이 어느 바인딩을 가리키는지 한 번만 정한 뒤, 그 답을 컴파일러의 나머지 부분 전체에 그대로 전달하고자 합니다.

이것이 리네이머의 첫 번째 일입니다. 두 번째 일은 보조 함수를 그것을 사용하는 함수보다 먼저 작성해야 하는 부담에서 우리를 해방하는 것입니다. 어떤 함수들은 서로를 호출하기까지 하므로, 모든 보조 함수를 앞에 두는 순서는 애초에 존재할 수 없습니다. 우리는 의존성 그래프에서 그러한 그룹을 찾아내고, 타입 검사기에 그룹 전체를 한꺼번에 넘겨줄 것입니다.

현재의 언어

최상위 선언과 지역 선언은 자신이 속한 블록 안에서 뒤에 나오는 선언을 참조할 수 있습니다. 다음 두 함수는 같은 재귀 컴포넌트에 속합니다:

even n = if n == 0 then True else odd (n - 1)

odd n = if n == 0 then False else even (n - 1)

main = even 100

각 바인딩 블록은 하나의 렉시컬 스코프입니다. 지역 정의는 철자가 같은 전역 정의를 가립니다. 같은 블록 안에 정의가 두 개 있으면 E0102 오류입니다. 재귀 컴포넌트의 모든 멤버는 우변이 함수여야 합니다. 따라서 a = b; b = a는 두 값 중 어느 것도 사용되지 않더라도 E0120으로 거부됩니다.

결합성 선언도 스코프 검사를 받습니다. 최상위 결합성 선언에는 파일 안에 대응하는 정의가 있어야 합니다(E0130). 바인딩되지 않은 변수는 E0101을 보고하며, 편집 거리가 2 이하이면 스코프 안에 있는 철자를 제안합니다.

이론

바인딩 식별자는 인턴된 철자와 새로 발급한 정수로 이루어집니다. 철자는 진단 메시지에 쓰입니다. 고유한 정수는 가려진 바인더들을 서로 구별합니다. 추론과 Core는 이 식별자를 사용하므로, 둘 다 렉시컬 조회를 반복할 필요가 없습니다. 이것은 3장에서 살펴본 바인딩 구분에 대응하는 컴파일러 쪽 장치입니다 (Pierce 2002).

각 함수에서 같은 블록 안에서 그 함수가 사용하는 각 바인딩으로 화살표를 그립니다. even이 odd를 호출하고 odd가 even을 호출한다면, 어느 쪽에서 출발하든 화살표를 따라 다른 쪽을 거쳐 다시 돌아올 수 있습니다. 이 둘은 한 묶음입니다. 강하게 연결된 컴포넌트(strongly connected component)는 이러한 상호 도달 가능성을 갖는 극대 그룹입니다.

이제 각 그룹을 하나의 노드로 취급합시다. 그 결과로 얻는 그래프에는 사이클이 없으므로, 의존 대상을 먼저 처리할 수 있습니다. 그것이 바로 우리 타입 검사기에 필요한 순서입니다. 재귀 그룹 밖에 있는 보조 함수는 호출자가 사용하기 전에 타입을 일반화할 수 있습니다 (Simon Marlow 2010).

재귀 컴포넌트 안에서는 바인더마다 임시 단형 타입을 하나씩 할당하고, 그것들을 기준으로 모든 본문을 추론하십시오. 그 임시 타입들을 추론 결과와 단일화하십시오. 그런 다음에야 컴포넌트를 일반화하십시오. 컴포넌트 밖에서는 각 출현이 결과 타입 스킴을 독립적으로 인스턴스화할 수 있습니다. 여기서도 시그니처는 추론된 타입을 검사합니다. 시그니처가 이끄는 다형 재귀는 14장에서 다룰 일로 남아 있습니다.

구현

이름 해석

표면 트리는 리네이밍 단계까지 지역 선언 블록 전체를 그대로 유지합니다. 리네이머는 우변을 하나라도 방문하기 전에 그 블록의 식별자를 모두 할당합니다. 각 람다는 자신의 매개변수를 담은 더 작은 스코프를 도입합니다. 조회는 가장 안쪽 스코프부터 바깥쪽으로 스코프를 탐색합니다.

#![allow(unused)]
fn main() {
pub fn rename(
    sess: &mut Session,
    program: ast::Program,
) -> Result<Program, Stop> {
    let program = crate::fixity::resolve(sess, program)?;
    for fixity in &program.fixities {
        for op in &fixity.operators {
            if !program.bindings.iter().any(|b| b.name == *op) {
                sess.emit(Diagnostic::error(
                    ErrorCode::OrphanFixity,
                    format!("fixity without a definition: {op}"),
                    fixity.span,
                ));
            }
        }
    }
    let mut primitives = Vec::new();
    let mut prelude = BTreeMap::new();
    if !sess.options.no_prelude {
        for op in [
            Op::Div,
            Op::Mod,
            Op::Add,
            Op::Sub,
            Op::Mul,
            Op::Append,
            Op::Eq,
            Op::Lt,
            Op::Le,
        ] {
            let id = sess.fresh_id(op.text());
            prelude.insert(op.text().into(), id.clone());
            primitives.push((op, id));
        }
    }
    let mut renamer = Renamer {
        sess,
        scopes: vec![prelude],
    };
    let groups = renamer.bindings(program.bindings);
    let main = renamer.lookup("main", program.span);
    renamer.sess.finish(Program {
        groups,
        primitives,
        main,
        span: program.span,
    })
}
}

리네이밍된 트리는 별도의 IR입니다. 그 출현(occurrence)에는 Id가 들어 있으며, 이것은 Name과 그 진단용 표기를 함께 보관합니다. 덤프는 이름을 x#12처럼 표시합니다. 이처럼 약간 반복되는 텍스트 덕분에 소스 세션이 사라진 뒤에도 덤프를 읽을 수 있습니다. 비교에는 여전히 할당된 식별자가 포함됩니다.

오류는 순회하는 동안 누적됩니다. 해석되지 않은 출현에는 새로운 자리 표시자가 주어지므로, 다른 본문들도 계속해서 스코프 오류를 검사할 수 있습니다. 드라이버는 자리 표시자가 평가 단계에 도달하기 전에, 리네이밍 패스 직후 멈춥니다.

의존성 컴포넌트

Tarjan의 탐색은 각 정점에 인덱스를 부여하고, 탐색 스택에서 아직 활성 상태인 것 중 도달 가능한 가장 낮은 인덱스를 기록합니다. 두 인덱스가 일치하면 그 정점은 완전한 컴포넌트의 루트이며, 이 컴포넌트는 한꺼번에 꺼낼 수 있습니다.

#![allow(unused)]
fn main() {
fn visit(&mut self, v: usize) {
    self.index[v] = Some(self.next);
    self.low[v] = self.next;
    self.next += 1;
    self.stack.push(v);
    self.active[v] = true;
    for &w in &self.edges[v] {
        if self.index[w].is_none() {
            self.visit(w);
            self.low[v] = self.low[v].min(self.low[w]);
        } else if self.active[w] {
            self.low[v] = self.low[v].min(self.index[w].unwrap());
        }
    }
    if self.low[v] == self.index[v].unwrap() {
        let mut group = Vec::new();
        loop {
            let w = self.stack.pop().unwrap();
            self.active[w] = false;
            group.push(w);
            if w == v {
                break;
            }
        }
        group.sort_unstable();
        self.result.push(group);
    }
}
}

우리의 간선은 의존 대상 쪽을 가리키므로, 완성된 컴포넌트는 추론에 필요한 순서대로 도착합니다. 우리는 덤프를 재현 가능하게 유지하기 위해 소스 인덱스를 고정된 순서로 방문합니다. 바인딩 하나만 담은 그룹은 그 바인딩이 자기 자신을 참조할 때에만 재귀적입니다. Tarjan의 부기(bookkeeping)를 검증하기 위해, 테스트는 상호 도달 가능성을 검사하는, 훨씬 느리지만 직접적인 방법으로도 컴포넌트를 찾습니다.

그룹 단위 추론

재귀 컴포넌트를 추론하는 동안 환경에는 모든 임시 타입이 들어 있습니다. 일반화는 모든 본문이 제약을 제공할 때까지 기다립니다. 시그니처는 추론된 스킴과 대조하여 검사되며, 내보낼 스킴이 확정되기 전에 특수화가 공유 컴포넌트 전체로 전파됩니다.

#![allow(unused)]
fn main() {
fn group(
    &mut self,
    env: &mut Vec<(Id, Scheme)>,
    group: &BindGroup,
) -> Result<(), Diagnostic> {
    self.level += 1;
    let size = env.len();
    let provisional: Vec<_> = group
        .binds
        .iter()
        .map(|b| (b.id.clone(), self.fresh()))
        .collect();
    if group.recursive {
        env.extend(
            provisional
                .iter()
                .map(|(id, ty)| (id.clone(), Scheme::mono(ty.clone()))),
        );
    }
    let inferred = group
        .binds
        .iter()
        .zip(&provisional)
        .map(|(b, (_, expected))| {
            let ty = self.infer(env, &b.rhs)?;
            if group.recursive {
                self.unify(expected, &ty, &b.rhs)?;
            }
            Ok(ty)
        })
        .collect::<Result<Vec<_>, Diagnostic>>();
    env.truncate(size);
    self.level -= 1;
    let inferred = inferred?;
    for (binding, ty) in group.binds.iter().zip(&inferred) {
        if let Some(annotation) = &binding.annotation {
            let scheme = self.generalize(ty.clone());
            self.check_signature(&scheme, annotation, &binding.rhs)?;
        }
    }
    self.level += 1;
    for (binding, ty) in group.binds.iter().zip(&inferred) {
        if let Some(annotation) = &binding.annotation {
            let declared = self.instantiate(annotation);
            self.unify(ty, &declared, &binding.rhs)?;
        }
    }
    self.level -= 1;
    for (binding, ty) in group.binds.iter().zip(inferred) {
        env.push((binding.id.clone(), self.generalize(ty)));
    }
    Ok(())
}
}

이 마지막 단계가 중요합니다. f와 g가 서로를 호출하고 f에 Int -> Int 시그니처가 있다면, 공유된 인자 타입은 g도 제약해야 합니다. g에 대해 제약 없는 다형 스킴을 내보내는 것은 건전하지 않습니다. 회귀 테스트는 그 뒤에 오는 g True 호출을 거부합니다.

재귀 클로저

인터프리터는 모든 재귀 그룹에 대해 불변 레시피, 즉 그 그룹의 람다 본문들과 바깥 환경을 저장합니다. 이러한 클로저 중 하나에 진입하면 함수 인자를 추가하기 전에 그룹의 바인딩을 재구성합니다. 어떤 클로저도 자기 자신을 가리키는 가변 포인터가 필요하지 않으며, 참조 카운팅 순환도 생기지 않습니다.

#![allow(unused)]
fn main() {
fn recursive_values(bindings: Rc<Vec<Bind>>, env: Rc<Env>) -> Env {
    let mut values = Vec::new();
    for binding in bindings.iter() {
        let Expr::Lam(parameter, body) = &*binding.rhs else {
            unreachable!("recursive lambda invariant")
        };
        let value = Value::Closure {
            parameter: parameter.clone(),
            body: Rc::new(*body.clone()),
            env: env.clone(),
            recursive: Some(bindings.clone()),
        };
        values.push((binding.name.clone(), value));
    }
    values
}
}

최상위 값 초기화식은 의존 순서에 따라 한 번 실행됩니다. 함수는 클로저입니다. 클로저를 생성해도 그 본문은 실행되지 않습니다. 이제 Core 린트는 스코프, 프리미티브 포화, 재귀 람다 제한뿐만 아니라 고유한 바인더 식별자도 검사합니다.

실행해 보기

$ cd crates/ch09_rename
$ cargo run -q -- run tests/programs/rename/mutual.fun
True
$ cargo run -q -- dump --pass=rename tests/programs/fun-forward.fun
nonrec { later#10 = 1; }
nonrec { main#9 = later#10; }
$ cargo xtask check-inherit 09
chapter 09: 101 inherited programs verified and chapter tests passed

작은 프로그램에서 보조 함수를 호출하는 함수 아래로 옮겨 보십시오. 이제는 잘 동작합니다. 그다음 두 함수가 서로를 호출하게 만들고 rename 덤프를 살펴보십시오. 재귀 그룹 하나가 보일 것이며, 이 그룹은 검사기에게 두 함수를 함께 추론하라고 알려 줍니다.

연습 문제

  1. 입문. 중첩된 섀도잉 예제에서 x가 나타나는 각 위치에 그것이 참조하는 바인더를 표시하십시오. rename 덤프와 비교해 보십시오.
  2. 중급. 재귀적이지 않은 보조 함수 하나가 딸린, 세 함수로 이루어진 재귀 그룹의 의존성 그래프를 그린 다음, 출력될 컴포넌트 순서를 예측하십시오.
  3. 중급. 서로 무관한 철자 오류 이름 두 개가 들어 있는 진단 테스트를 추가하고, 이 패스가 두 오류를 모두 보고하는지 확인하십시오.
  4. 고급. 불변 클로저 레시피에 속한 함수 중 하나가 다른 클로저를 반환하더라도 참조 카운팅 순환이 생기지 않는 이유를 설명하십시오.
  5. 고급. 시그니처 기반 다형 재귀를 지원하려면 현재의 컴포넌트 구성 방식과 검사 전략 중 어디를 바꿔야 하는지 짚어 보십시오.

더 읽을거리

Haskell Report는 바인딩 그룹과 의존성 분석을 설명합니다 (Simon Marlow 2010). Pierce는 변수, 스코프, 바인딩에 관한 기초적인 내용을 다룹니다 (Pierce 2002).

10장: 타입이 있는 Core와 Core 린트

새 파일

  • src/core_lint.rs
  • src/core_type.rs

변경된 파일

  • src/core.rs
  • src/elaborate.rs
  • src/typecheck.rs
  • src/eval.rs
  • src/lib.rs
  • src/main.rs
  • src/rename.rs
  • src/renamed.rs

동기

타입 검사기가 우리 프로그램을 받아들였습니다. 훌륭합니다. 이제 컴파일러가 프로그램을 다시 쓸 차례인데, 이 과정에서 우리 스스로 실수를 끼워 넣을 기회가 얼마든지 생깁니다. 잘못된 인자를 넘기거나, 타입 인스턴스화를 빠뜨리거나, 변수 정체성을 재사용할 수도 있습니다. 소스 프로그램이 올바르더라도 우리가 그것을 잘못 번역하면 아무 소용이 없습니다.

우리는 Core에 명시적인 타입을 부여하고 이를 위한 두 번째 검사기를 작성하겠습니다. 이 검사기가 맡은 일은 추론보다 쉽습니다. 모든 답이 이미 트리 안에 있으므로, 검사기는 그 답들이 서로 맞아떨어지는지만 확인하면 됩니다. 변환이 잘못되면 문제가 일어난 곳 가까이에서 그것을 잡아낼 수 있습니다. 자기 숙제를 스스로 검사하는 컴파일러는 꽤 쓸모 있는 존재입니다.

현재의 언어

우리는 여전히 타입 인자를 일일이 적지 않고도 다형 함수를 작성할 수 있습니다.

id x = x

number = id 42

main = id True

Core에서 id의 정의는 타입 변수에 대해 추상화합니다. 두 번의 사용에서는 이 함수를 먼저 Int에, 그다음 Bool에 적용합니다. 이러한 적용은 타입이 없는 식에 붙은 주석이 아니라 실제 IR 노드입니다. 평가기는 그 타입들을 무시하므로, 이 프로그램은 여전히 True를 출력합니다.

이론

소스 표현식 id 42에는 작은 결정 하나가 숨어 있습니다. 우리는 항등 함수를 Int에서 사용하고 있습니다. System F는 그 결정을 프로그램 안에 써 넣는 방법을 제공합니다. 타입 추상화(type abstraction)는 타입 매개변수를 도입하고, 타입 적용(type application)은 그 매개변수에 쓸 타입을 고릅니다. Girard와 Reynolds가 다형성을 이렇게 명시적으로 다루는 방식을 개발했습니다 [Girard (1972); Reynolds (1974)].

새로운 두 형태를 일반적인 람다 및 호출과 나란히 놓고 생각해 봅시다. 일반적인 람다는 값을 기다립니다. 타입 추상화는 타입에 대해 추상화합니다. 타입 인자는 런타임 전에 사라지지만, 이 비유는 규칙을 읽는 데 도움이 됩니다.

이 장에서는 모든 타입 바인더의 카인드가 *입니다. 고차 카인드 생성자는 나중에 카인드 판단을 확장할 것입니다. 일반적인 적용에는 여전히 화살표 타입이 필요합니다. 전칭 한정된 함수는 먼저 자신의 타입 인자를 받아야 합니다.

Core는 System F 전체를 지원합니다. Fun은 여전히 HM 다형성만 드러냅니다. 손으로 만든 Core 항을 사용하면 소스 구문을 확장하지 않고도 그 차이를 시험할 수 있습니다 (Pierce 2002).

구현

Core 데이터 타입

Rust 코드를 보기 전에 같은 언어를 작은 문법으로 적어 볼 수 있습니다. 각 세로 막대는 “또는”으로 읽으십시오. 표현식 는 아래 형태 중 하나이며, 오른쪽 열은 각 형태가 무엇을 하는지 알려 줍니다.

여기서 와 는 각각 지역 값과 전역 값을 가리키고, 은 리터럴이며, 는 프리미티브입니다. 윗줄은 시퀀스를 뜻하므로, 는 프리미티브의 피연산자들을 평가 순서대로 나타냅니다. 표기법에서는 소스 스팬을 생략하지만, Rust 표현은 진단을 위해 이를 유지합니다.

타입과 바인딩 그룹에는 각각의 문법이 있습니다.

재귀 그룹의 이름은 그 그룹의 우변 전체에서 스코프 안에 있습니다. 린트는 또한 그 우변들이 함수를 나타내는지 검사하며, 이 함수는 타입 추상화로 감싸여 있을 수도 있습니다. 비재귀 바인딩에서는 새 이름을 let의 본문에서만 사용할 수 있습니다.

두 가지 람다가 서로 다르다는 점에 주목하십시오. 에서 대문자 는 어떤 값도 고르지 않습니다. 이것은 일반적인 가 사용하는 타입 매개변수를 도입합니다. 이 항등 함수를 타입에 적용한 다음 값에 적용하면 가 됩니다. 이것이 우리가 처음에 살펴본 소스 수준의 id 42를 명시적으로 나타낸 버전입니다.

Core 식은 Rust 열거형(enum)입니다. 각 변수와 항 바인더는 자신의 타입을 담은 Id를 지니므로, 린트는 추론을 다시 실행하지 않고도 트리를 검사할 수 있습니다.

#![allow(unused)]
fn main() {
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Int(i64),
    Bool(bool),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    If(Box<Expr>, Box<Expr>, Box<Expr>),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    pub main: Id,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::If(c, a, b) => write!(f, "(if {c} {a} {b})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Int(n) => write!(f, "{n}"),
            Self::Bool(b) => {
                write!(f, "{}", if *b { "True" } else { "False" })
            }
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
}

식 생성자는 항 수준의 함수(Lam, App)와 타입 수준의 추상화 및 인스턴스화(TyLam, TyApp)를 구별합니다. Let은 리네이머가 만들어 낸 재귀 컴포넌트를 유지합니다. Prim은 명시적인 타입 인자, 평가된 피연산자, 그리고 실패 시 보고할 소스 스팬을 지닙니다.

#![allow(unused)]
fn main() {
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Int(i64),
    Bool(bool),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    pub main: Id,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::If(c, a, b) => write!(f, "(if {c} {a} {b})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Int(n) => write!(f, "{n}"),
            Self::Bool(b) => {
                write!(f, "{}", if *b { "True" } else { "False" })
            }
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    If(Box<Expr>, Box<Expr>, Box<Expr>),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
}

바인딩 그룹은 타입이 붙은 이름과 우변을 담습니다. 프로그램은 이러한 그룹들을 순서대로 나열한 목록과 진입점으로 이루어집니다:

#![allow(unused)]
fn main() {
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Int(i64),
    Bool(bool),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    If(Box<Expr>, Box<Expr>, Box<Expr>),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    pub main: Id,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::If(c, a, b) => write!(f, "(if {c} {a} {b})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Int(n) => write!(f, "{n}"),
            Self::Bool(b) => {
                write!(f, "{}", if *b { "True" } else { "False" })
            }
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Int(i64),
    Bool(bool),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    If(Box<Expr>, Box<Expr>, Box<Expr>),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    pub main: Id,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::If(c, a, b) => write!(f, "(if {c} {a} {b})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Int(n) => write!(f, "{n}"),
            Self::Bool(b) => {
                write!(f, "{}", if *b { "True" } else { "False" })
            }
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Int(i64),
    Bool(bool),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    If(Box<Expr>, Box<Expr>, Box<Expr>),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::If(c, a, b) => write!(f, "(if {c} {a} {b})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Int(n) => write!(f, "{n}"),
            Self::Bool(b) => {
                write!(f, "{}", if *b { "True" } else { "False" })
            }
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    pub main: Id,
}
}

이 장에는 아직 If가 있습니다. 데이터 타입 생성자와 패턴 매치는 11장에서, 평탄한 case와 조인 포인트는 16장에서 등장합니다. 여기서는 더 작은 열거형을 사용하므로, 그러한 형태들을 추가하기 전에 타입이 있는 정교화와 린트를 먼저 확립할 수 있습니다.

추론 결과 기록하기

이제 이름이 바뀐 식에는 안정적인 노드 번호가 있습니다. 검사기는 각 변수 출현에서 사용된 새 타입 인자, 각 바인딩의 스킴, 각 람다 매개변수의 단형 타입을 기록합니다. 검사기는 반환하기 전에 이 모든 타입을 zonk하므로, 정교화 단계에서는 가변 메타변수 저장소에 접근할 필요가 전혀 없습니다.

재귀 그룹 안에서 추론된 변수 출현은 처음에는 임시 단형 타입을 보게 됩니다. 컴포넌트가 일반화되고 나면, 정교화 단계는 그 컴포넌트의 재귀적 Core 참조를 대응하는 일반화된 변수로 인스턴스화합니다. 일반적인 출현은 추론 중에 기록된 새 인자를 사용합니다.

다형성 정교화하기

일반화된 각 메타변수는 새로운 Core 타입 식별자를 받습니다. 바인더 타입은 변환된 단형 타입을 Forall로 감쌉니다. 우변은 자신의 항을 그에 맞는 TyLam들로 감쌉니다. 지역 스킴은 여전히 바깥 함수의 추상화가 묶은 타입 변수를 언급할 수 있습니다.

#![allow(unused)]
fn main() {
fn group(
    &mut self,
    group: &renamed::BindGroup,
    outer: &BTreeMap<u32, u32>,
    global: bool,
) -> BindGroup {
    for binding in &group.binds {
        self.binder(
            binding.id.clone(),
            &self.typed.schemes[&binding.id],
            outer,
            global,
        );
    }
    let binds = group
        .binds
        .iter()
        .map(|binding| {
            let binder = self.binders[&binding.id].clone();
            let mut variables = outer.clone();
            variables.extend(binder.quantifiers.iter().copied());
            let mut rhs = self.expression(&binding.rhs, &variables);
            for (_, variable) in binder.quantifiers.iter().rev() {
                rhs = Expr::TyLam(*variable, Box::new(rhs));
            }
            Bind {
                name: binder.id,
                rhs: Box::new(rhs),
            }
        })
        .collect();
    BindGroup {
        recursive: group.recursive,
        binds,
    }
}
}

출현에는 TyApp이 붙고, 람다 매개변수는 변환된 타입을 지닙니다. Core는 전역 참조와 지역 변수를 구별합니다. Core의 바인더 표기는 선언된 환경에 대조하여 검사되므로, 다른 부분이 잘못된 출현에 그럴듯해 보이는 타입을 붙여도 린트를 피해 갈 수 없습니다.

둘러싼 어느 스킴에도 없는 내부 변수에는 닫힌 증거(witness)가 필요합니다. (\f -> 42) (\x -> x)가 그러한 예입니다. 우리는 이렇게 지워지는 표기에 Int를 사용합니다. 이는 소스 식에 기본 타입을 정해 주거나 모호한 클래스 제약을 해소하는 것이 아닙니다.

독립적인 린트

검사기는 항 스코프와 타입 스코프를 유지하고, 이미 사용된 바인더 식별자들의 집합도 함께 관리합니다. 검사기는 화살표 적용, 프리미티브 포화, 조건문 분기, 재귀 함수 그룹, 바인더 표기를 검사합니다. 다음은 명시적 타입 추상화 규칙과 타입 적용 규칙입니다:

#![allow(unused)]
fn main() {
Expr::TyLam(variable, body) => {
    if !self.seen_types.insert(*variable) {
        return Err(format!(
            "duplicate Core type binder t{variable}"
        ));
    }
    self.types.insert(*variable);
    let body = self.expression(body)?;
    self.types.remove(variable);
    Ok(Type::Forall(*variable, Box::new(body)))
}
Expr::TyApp(fun, argument) => {
    self.well_formed(argument)?;
    let function = self.expression(fun)?;
    let Type::Forall(variable, body) = function else {
        return Err(format!(
            "Core type application of non-polymorphic type {function}"
        ));
    };
    Ok(body.substitute(variable, argument))
}
}

타입 적용은 한정된 본문에 치환을 수행합니다. 치환은 인자에 있는 자유 타입 변수를 포획하지 않아야 하며, 이는 우리 일래버레이터 밖에서 구성된 Core 항에 대해서도 마찬가지입니다. 구현은 충돌하는 바인더를 새 이름으로 바꿉니다:

#![allow(unused)]
fn main() {
pub fn substitute(&self, variable: u32, replacement: &Type) -> Type {
    match self {
        Self::Var(v) if *v == variable => replacement.clone(),
        Self::Arrow(a, b) => Self::arrow(
            a.substitute(variable, replacement),
            b.substitute(variable, replacement),
        ),
        Self::Forall(v, _) if *v == variable => self.clone(),
        Self::Forall(v, body) => {
            let (v, body) = if replacement.free().contains(v) {
                let fresh = self
                    .largest()
                    .max(replacement.largest())
                    .max(variable)
                    .checked_add(1)
                    .expect("type variable supply exhausted");
                (fresh, body.substitute(*v, &Self::Var(fresh)))
            } else {
                (*v, *body.clone())
            };
            Self::Forall(
                v,
                Box::new(body.substitute(variable, replacement)),
            )
        }
        _ => self.clone(),
    }
}
}

타입 동등성은 알파 동치이므로, 두 forall 타입이 서로 다른 새 식별자를 사용하더라도 같은 타입을 나타낼 수 있습니다. 반면에 자유 타입 변수는 반드시 같은 바깥 바인더를 가리켜야 합니다.

#![allow(unused)]
fn main() {
pub fn lint(program: &Program) -> Result<(), String> {
    let mut checker = Checker::default();
    for group in &program.groups {
        checker.group(group, true)?;
    }
    checker.expression(&Expr::Global(program.main.clone()))?;
    Ok(())
}
}

여담. 다형 항등 함수의 적용에서 TyApp을 빠뜨리는 컴파일러 버그는 Core application of non-function type forall t0. t0 -> t0을 만들어 냅니다. 소스 프로그램은 올바를 수 있습니다. 생성된 Core는 그렇지 않습니다. 잘못된 Core 테스트는 소스 추론을 다시 실행하지 않고 이 실패를 직접 검사합니다.

소거와 엄격한 실행

TyLam과 TyApp은 런타임 객체를 할당하지 않습니다. 인터프리터는 런타임 값을 치환하지 않고 이들을 그대로 지나갑니다. 이것은 System F를 위한 별도의 런타임 평가 전략이 아니라 소거입니다.

특히 타입 추상화는 엄격한 초기화식을 지연시키지 않습니다. 아래의 사용되지 않는 다형 바인딩은 main이 선택되기 전에 여전히 실패합니다:

unused = (\x -> \y -> y) (div 1 0)

main = 42

18장에서는 명시적으로 소거된 IR을 도입합니다. 코퍼스 실행은 이미 --lint를 활성화합니다.

실행해 보기

$ cd crates/ch10_core
$ cargo run -q -- run tests/programs/core/self-instantiation.fun --lint
42
$ cargo run -q -- dump --pass=core --no-prelude tests/programs/layout/signature.fun
nonrec { id#0 : forall t0. t0 -> t0 = (type-lambda t0 (lambda x#2 : t0 x#2)); }
nonrec { main#1 : Int = (app (type-app @id#0 Int) 42); }
$ cargo test -p ch10_core --test system_f

유용한 실험으로, 작은 Core 항에서 타입 적용 하나를 제거한 뒤 린트를 실행해 보십시오. 이 테스트에서 소스 검사기는 아무 역할도 하지 않습니다. 우리는 컴파일러만이 저지를 수 있었던 실수를 두 번째 검사기에 일부러 넣고 있습니다. 두 번째 검사기는 평가 전에 그 항을 거부해야 합니다.

연습 문제

  1. 입문. id id 42에 대한 Core 덤프에서 모든 타입 추상화와 타입 적용을 찾아보십시오.
  2. 중급. 포획된 바깥 타입 변수가 지역 바인딩의 스킴에서는 자유 변수로 남지만 완전한 Core 프로그램에서는 속박되는 이유를 설명하십시오.
  3. 중급. 인자 개수는 맞지만 피연산자 타입이 잘못된, 잘못된 Core 프리미티브 호출을 구성해 보십시오.
  4. 고급. 새 이름 부여를 제거했을 때 타입 치환에서 발생하는 포획 버그를 보이고, 알파 동치만으로는 이를 바로잡을 수 없는 이유를 설명하십시오.

더 읽을거리

명시적 다형성의 원전은 지라르(Girard)의 학위 논문과 레이놀즈(Reynolds)의 논문입니다 [Girard (1972); Reynolds (1974)]. 피어스(Pierce)는 System F의 타입 규칙과 치환 규칙을 전개합니다 (Pierce 2002).

11장: 데이터 타입과 패턴 매칭

새 파일

  • src/data.rs
  • src/syn.rs
  • src/desugar.rs

변경된 파일

  • src/typecheck.rs
  • src/core.rs
  • src/elaborate.rs
  • src/ast.rs
  • src/core_lint.rs
  • src/core_type.rs
  • src/eval.rs
  • src/fixity.rs
  • src/layout.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/parse.rs
  • src/rename.rs
  • src/renamed.rs
  • src/session.rs
  • src/types.rs

동기

정수와 함수만으로도 우리는 꽤 멀리 왔지만, 이제 제대로 된 자료 구조를 만들 때입니다. 이진 트리는 비어 있을 수도 있고, 값 하나와 하위 트리 두 개를 담을 수도 있습니다. 조회는 실패할 수도 있고 결과를 돌려줄 수도 있습니다. 대수적 데이터 타입을 쓰면 “여기에는 아무것도 없음”을 뜻하는 특별한 값을 따로 만들어 내지 않고도 이런 것들을 직접 표현할 수 있습니다.

패턴 매칭은 그 값들을 분해하는 똑같이 직접적인 방법을 제공합니다. 데이터의 모양이 곧 프로그램의 모양이 됩니다. 이것은 함수형 프로그래밍의 아름다운 부분 중 하나이며, 우리는 지금부터 이것을 끊임없이 사용할 것입니다.

우리는 먼저 패턴을 소스 순서대로 시도하는 방식으로 매칭을 구현하겠습니다. 이렇게 하면 매칭이 무엇을 의미하는지 명확하게 설명할 수 있습니다. 16장에서는 같은 매칭을 네이티브 코드에 적합한 결정 트리로 바꾸겠습니다.

현재의 언어

선언은 명목적(nominal) 타입과 그 생성자를 도입합니다. 생성자 인자는 엄격(strict)하게 평가되며, 생성자는 커리된 함수로 주고받을 수 있습니다.

data Tree a = Empty | Node (Tree a) a (Tree a)

size Empty = 0
size (Node left _ right) = 1 + size left + size right

main = size (Node Empty 42 Empty)

size를 데이터 타입과 나란히 놓고 읽어 보십시오. Empty는 0을 더합니다. Node는 1에 두 자식의 크기를 더한 값을 더합니다. 정의가 데이터를 거의 그대로 따라갑니다. 아름다운 일이며, 훨씬 더 흥미로운 트리로도 확장됩니다.

트리를 뒤집는 것도 똑같이 직접적으로 할 수 있습니다.

data Tree a = Empty | Node (Tree a) a (Tree a)

mirror Empty = Empty
mirror (Node left x right) = Node (mirror right) x (mirror left)

main = mirror (Node Empty 1 (Node Empty 2 Empty))

mirror가 x를 전혀 들여다보지 않는다는 점에 주목하십시오. 그 타입은 Tree a -> Tree a이므로, 어떤 원소 타입의 트리에도 동작합니다. 재귀는 값은 그대로 둔 채 모양만 바꿉니다.

등식에는 중첩된 생성자 패턴, 리터럴, 와일드카드, as-패턴이 올 수 있습니다. 리스트, 쌍, 세 쌍, 유닛에는 전용 구문이 있습니다. 가드는 Bool 타입이어야 합니다. 모든 가드가 실패하면 매칭은 다음 등식으로 이어집니다.

select all@(x : xs) | x < 0 = xs
select all = all

main = select [-1,2,3]

where 블록은 모든 가드를 포함한 등식 전체에 속합니다. case 대안에도 가드와 where 블록을 둘 수 있습니다. 패턴 람다와 엄격한 비재귀 패턴 바인딩도 동작합니다: let (x,y) = (20,22) in x + y. 여기서 타입은 여전히 1차(first-order)입니다. 데이터 타입 매개변수는 카인드 *를 가지므로, Tree Int는 유효하지만 f Int처럼 매개변수를 적용하는 것은 13장에서 다룹니다.

초기 Prelude는 다음의 실제 Fun 소스에서 임베드됩니다:

data Bool = False | True

data Ordering = LT | EQ | GT

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

data [] a = [] | (:) a [a]

data () = ()

data (,) a b = (,) a b

data (,,) a b c = (,,) a b c

이론

데이터 타입 선언은 각 생성자에 대한 다형적 타입 스킴을 제공합니다. 예를 들어 Node는 a에 대해 정량화된 Tree a -> a -> Tree a -> Tree a 타입을 가집니다. 다형 함수의 각 출현과 마찬가지로, 각 출현은 그 스킴을 새로운 메타변수로 인스턴스화합니다 [Marlow (2010); Pierce (2002)].

필드 타입이 인스턴스화된 생성자에 대해, 패턴 판단은 하위 패턴이 도입하는 바인딩을 반환합니다:

결합된 패턴 환경은 한 이름을 두 번 바인딩해서는 안 됩니다. 각 행은 같은 검사 대상(scrutinee) 타입들에 대해 검사되며, 성공한 모든 우변은 같은 결과 타입을 가져야 합니다. 가드는 추가로 Bool과 단일화됩니다.

Fun은 값을 구성하기 전에 필드를 평가합니다. 매치가 실패하면 런타임 오류가 발생합니다. 지연 패턴이나 보류된 생성자 필드는 없습니다. 완전성 경고와 중복 경고에는 별도의 분석이 필요합니다. 이 경고들은 매치 컴파일과 함께 도입됩니다(Maranget 2007).

구현

필드를 검사하기 전에 이름 선언하기

data.rs는 먼저 모든 타입 헤드를 할당한 다음 생성자 필드를 변환합니다. 이 덕분에 상호 재귀적인 데이터 타입이 가능합니다. 불변 테이블은 각 타입의 항수, 각 생성자의 스킴, 그리고 선언 순서에 따른 태그를 저장합니다. 중복된 이름, 바인딩되지 않은 필드 변수, 잘못된 타입 항수는 식 추론이 시작되기 전에 진단됩니다.

리네이머는 표현식과 패턴 안의 생성자 이름을 일반 변수와 별도로 해석합니다. 패턴 변수는 행마다 새로운 식별자를 받습니다. 그런 다음 가드와 우변을 모두 방문하는 동안 리네이머의 스코프에는 그 변수들과 해당 행의 지역 선언이 들어 있습니다.

구문을 Syn으로 낮추기

desugar.rs는 리네이밍과 추론 사이에 명시적인 패스를 도입합니다. 리스트와 튜플은 생성자 적용이 됩니다. if는 True와 False에 대한 두 행짜리 매치가 됩니다. 중위 &&와 ||는 먼저 조건식이 됩니다. 퍼스트 클래스(first-class) 값으로서 이 연산자들은 평범한 에타 확장 함수이며, 그 인자는 값에 의한 호출 적용으로 평가됩니다.

변수 패턴만 포함하는 단일 등식은 일반 람다와 let을 사용할 수 있습니다. 그 밖의 등식은 각 인자에 대한 람다와 그 뒤에 오는 매치가 됩니다:

#![allow(unused)]
fn main() {
let params: Vec<_> = (0..rows[0].patterns.len())
    .map(|_| self.sess.fresh_id("$match"))
    .collect();
let scruts = params
    .iter()
    .map(|id| self.node(Kind::Var(id.clone()), span))
    .collect();
let mut body = self.node(Kind::Match(scruts, rows), span);
for id in params.into_iter().rev() {
    body = self.node(Kind::Lam(id, Box::new(body)), span);
}
body
}

엄격한 패턴 바인딩은 바인딩된 값들을 중첩된 쌍으로 표현한 하나의 검사된 묶음을 사용하고, 그 뒤에 프로젝션이 이어집니다. 우변은 한 번만 평가됩니다. 새로 바인딩되는 이름들은 그 우변을 리네이밍하는 동안 가려지므로, 같은 철자의 바깥쪽 바인딩에 여전히 접근할 수 있습니다. 사용되지 않는 최상위 패턴 바인딩도 여전히 실행되며 실패할 수 있습니다.

생성자 패턴 추론하기

검사기는 생성자 스킴을 인스턴스화하고, 필드 화살표를 결과 타입과 분리한 뒤, 그 결과를 검사 대상과 단일화합니다. 그런 다음 각 하위 패턴을 대응하는 인스턴스화된 필드에 대해 검사합니다:

#![allow(unused)]
fn main() {
PatKind::Con(id, patterns) => {
    let info = self.data.con(id).clone();
    if patterns.len() != info.fields.len() {
        return Err(Diagnostic::error(
            ErrorCode::Mismatch,
            format!(
                "{} expects {} pattern fields",
                id.name,
                info.fields.len()
            ),
            pattern.span,
        ));
    }
    let mut ty = self.instantiate(&info.scheme);
    let mut fields = Vec::new();
    for _ in patterns {
        let Type::Arrow(a, b) = ty else {
            unreachable!("constructor arity")
        };
        fields.push(*a);
        ty = *b;
    }
    self.unify(expected, &ty, &site)?;
    for (pattern, ty) in patterns.iter().zip(fields) {
        self.pattern(env, pattern, &ty)?;
    }
}
}

패턴 변수는 단형적으로 행 환경에 들어갑니다. 지역 바인딩은 일반적인 let 다형성을 유지합니다. 검사기는 System F 정교화에 이미 필요한 인스턴스화 메타데이터와 함께 식과 패턴 바인더의 타입을 기록합니다.

Core와 린트 확장하기

정교화는 생성자 값을 에타 확장하여, 본문이 포화된 Con 노드인 람다로 만듭니다. 엄격한 let은 매치 검사 대상을 한 번씩, 소스 순서대로 평가합니다. Core는 결정 트리 컴파일러가 도입될 때까지 중첩 패턴을 유지합니다.

#![allow(unused)]
fn main() {
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}

/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}

/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}

/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
}

린트는 생성자 타입 인자, 필드 개수, 필드 타입을 독립적으로 검사합니다. 생성자 패턴의 경우 린트는 검사 대상의 타입 인자로 선언을 인스턴스화한 다음 각 바인더를 검사합니다. 가드는 불리언이어야 하며, 모든 행은 선언된 매치 결과를 내야 합니다. 형식이 잘못된 Core 테스트는 표면 추론기에 입력 검증을 요청하지 않고도, 마치 연습 문제를 풀듯 이러한 검사들을 직접 시험합니다.

행 실행하기

인터프리터는 생성자 필드를 왼쪽에서 오른쪽으로 평가합니다. 이미 평가된 값에 대한 매칭은 생성자와 리터럴을 재귀적으로 검사하며, 변수와 as-패턴으로 행 전용 환경을 확장합니다:

#![allow(unused)]
fn main() {
fn matches_pattern(p: &core::Pat, v: &Value, env: &mut Env) -> bool {
    match p {
        core::Pat::Wild => true,
        core::Pat::Var(id) => {
            env.push((id.clone(), v.clone()));
            true
        }
        core::Pat::As(id, p) => {
            env.push((id.clone(), v.clone()));
            matches_pattern(p, v, env)
        }
        core::Pat::Lit(l) => matches!(v,Value::Lit(actual) if l==actual),
        core::Pat::Con(id, ps) => match v {
            Value::Con(actual, vs)
                if id == actual && ps.len() == vs.len() =>
            {
                ps.iter().zip(vs).all(|(p, v)| matches_pattern(p, v, env))
            }
            _ => false,
        },
    }
}
}

패턴이 실패하면 그 환경을 버리고 다음 행을 시도합니다. 패턴이 성공하면 그 where 그룹을 한 번 평가한 다음, 가드를 순서대로 검사합니다. 모든 가드가 실패하면 실행은 다음 행으로 넘어갑니다. 행을 모두 소진하면 fun: pattern match failure at FILE:LINE:COL을 보고하고 상태 코드 1로 종료합니다.

값 프린터는 생성자 우선순위를 사용하므로 중첩된 적용은 Just (Left 3)처럼 출력됩니다. 리스트와 튜플은 표면 표기법을 사용합니다. 문자 리스트는 ['a','b']로 출력되고, 기본 문자열은 큰따옴표를 유지합니다.

실행해 보기

$ cd crates/ch11_data
$ cargo run -q -- run tests/programs/data/tree.fun --lint
(True,False)
$ cargo run -q -- dump --pass=syn tests/programs/data/bool-data.fun
nonrec { not#9 = (lambda $match#11 (match $match#11 [ (True) -> False ] [ (False) -> True ])); }
nonrec { main#10 = (app (app (,) (app not#9 True)) (app not#9 False)); }
$ cargo test --test data

가드 폴스루는 실험해 볼 가치가 있습니다. 첫 번째 패턴은 매치되지만 그 가드는 실패하게 만든 다음, 다음 등식이 실행되는지 확인하십시오. 사용되지 않으면서 실패하는 최상위 패턴 바인딩도 시도해 보십시오. 엄격한 평가에서는 “사용되지 않는다“고 해서 매치가 사라지지 않습니다. 이러한 예제들은 평범하게 성공하는 트리 순회에서는 놓칠 수 있는 부분을 연습 문제처럼 짚어 줍니다.

연습 문제

  1. 입문. 이진 트리 미러 함수를 정의하고 비대칭 트리에서 테스트하십시오.
  2. 중급. 필드가 너무 적은 생성자 패턴에서 발생하는 타입 오류와, 서로 다른 타입을 반환하는 두 등식에서 발생하는 타입 오류를 설명하십시오.
  3. 중급. 뒤따르는 등식으로 폴스루하는 가드 두 개를 작성하십시오. 두 가드가 공유하는 계산을 where로 옮기고 Syn 덤프를 살펴보십시오.
  4. 고급. 엄격한 패턴 바인딩(strict pattern binding)을 Haskell의 일반 패턴 바인딩과 비교하십시오. 종료 여부나 오류 동작이 달라지는 프로그램을 작성하십시오.
  5. 심화. 필드 개수는 유지하면서 타입이 서로 다른 두 생성자 필드를 맞바꾸는 린트 테스트를 설계하십시오.

더 읽을거리

Haskell Report는 데이터 타입 선언, 패턴, 가드, 지역 스코프를 명세합니다(Simon Marlow 2010). Pierce는 데이터의 도입(introduction)과 제거(elimination) 사이의 연결을 전개합니다(Pierce 2002). Maranget의 논문들은 그 이후 단계인 경고 분석과 결정 트리 컴파일을 설명합니다[Maranget (2007); Maranget (2008)].

12장: 레코드

새 파일

없음.

변경된 파일

  • src/typecheck.rs
  • src/data.rs
  • src/lexer.rs
  • src/elaborate.rs
  • src/ast.rs
  • src/core.rs
  • src/desugar.rs
  • src/eval.rs
  • src/fixity.rs
  • src/format.rs
  • src/main.rs
  • src/parse.rs
  • src/rename.rs
  • src/renamed.rs

동기

생성자의 세 번째 인자는 무엇을 의미합니까? 작은 점이나 쌍이라면 아마 기억할 수 있을 것입니다. 정수 여섯 개쯤을 담은 설정값이라면 코드를 읽을 때마다 작은 기억력 시험이 됩니다. 그 필드들에 이름을 붙여 봅시다.

Fun의 레코드는 우리가 이미 가진 데이터 타입 위에 세워집니다. 필드를 읽을 때는 점 구문을 사용하고, 모든 레코드 타입은 생성자를 하나만 갖도록 요구하겠습니다. 이렇게 하면 전통적인 Haskell 선택자의 거북한 두 가지 성질을 피할 수 있습니다. 잘못된 생성자를 골랐다는 이유로 접근이 실패할 수 없으며, 필드 이름이 일반 함수 이름을 차지하지도 않습니다. 두 레코드 타입이 모두 x 필드를 가질 수 있습니다[Marlow (2010); Mitchell & Fletcher (n.d.)].

흥미로운 작업은 추론에 있습니다. p.x를 볼 때 우리는 p에 대해 얼마나 알고 있습니까? 표현식의 다른 곳에 있는 정보가 그 질문에 답하는 데 도움이 되도록 만들겠습니다.

현재의 언어

data Point = Point { x :: Int, y :: Int }

move p = p { x = p.x + 1 }

main = move (Point { y = 4, x = 3 })

생성 시에는 모든 필드를 정확히 한 번씩 제공해야 합니다. 생성식의 표현식들은 선언 순서와 다르더라도 작성된 순서대로 실행됩니다. 위치 기반 생성과 패턴도 여전히 사용할 수 있습니다. 레코드 패턴에서는 필드를 생략할 수 있으며, 생략된 필드는 와일드카드가 됩니다. Point { x = n }는 첫 번째 좌표만 바인딩합니다.

프로젝션과 갱신은 적용보다 더 강하게 결합합니다. 따라서 f p.x는 f (p.x)를 뜻하고, f p { x = 3 }는 f (p { x = 3 })를 뜻합니다. 섹션 (.x)는 람다의 축약 표기입니다. shape.origin.x와 같은 연쇄 접근은 프로젝션을 두 번 수행합니다. 갱신은 타입 인자를 포함하여 수신자의 완전한 타입을 그대로 보존합니다.

data Label = Label { x :: String }

coordinate :: Point -> Int
coordinate p = p.x

시그니처가 어느 x를 뜻하는지 식별해 줍니다. 괄호로 감싼 식 표기 (p :: Point).x도 사용할 수 있습니다. 어떤 필드를 선언한 레코드가 하나뿐이라면, 검사기는 표기 없이도 그 수신자 타입을 추론할 수 있습니다.

이론

필드가 동일한 두 선언이라도 서로 다른 타입을 기술합니다. 여기서 명목적(nominal)이라는 말은 바로 이런 뜻입니다. Point는 단지 둘 다 우연히 x와 y를 담고 있다는 이유만으로 다른 레코드가 되지 않습니다. 반면 구조적 시스템이나 행 다형성(row-polymorphic) 시스템이라면 함수가 필요한 필드를 가진 어떤 레코드든 받아들이게 할 수 있습니다(Pierce 2002). 우리는 필드 접근이 런타임 이전에 특정 생성자와 슬롯으로 해석될 수 있도록 명목적 설계를 택합니다.

프로젝션은 수신자 타입과 결과 타입을 잇는 요구 제약(wanted constraint)을 도입합니다.

Has는 내부적인 검사 의무입니다. 이것은 결코 타입 스킴으로 일반화되지 않습니다. 필드 이름이 유일하다면, 검사기는 새로운 매개변수를 사용하여 수신자를 해당 레코드 타입과 즉시 단일화합니다. 그렇지 않으면 수신자의 명목적 헤드가 밝혀질 때까지 기다립니다. 바인딩 그룹을 일반화하기 전에는 고정점에 도달할 때까지 해석을 반복합니다. 끝까지 남아 있는 필드 요구는 모두 E0245와 표기 힌트를 만들어 냅니다.

GHC의 record-dot 제안은 표면 구문에 대해 참고할 만한 선행 사례입니다. 그 제안의 HasField 메커니즘은 다른 형태의 오버로딩된 접근을 지원합니다. Fun 필드 솔버는 정교화 전에 항상 구체적인 명목적 레코드 하나를 선택합니다(Mitchell and Fletcher, n.d.).

구현

점 분류하기

이 토큰에서는 공백이 의미를 가집니다. f.g는 프로젝션을 뜻하지만 f . g는 일반 합성 연산자를 사용합니다. 후처리 패스는 트리비아 수집과 레이아웃 처리가 인접성을 흐리기 전에 원래 토큰의 스팬을 비교합니다.

#![allow(unused)]
fn main() {
/// Classify dot adjacency before trivia capture and layout can erase spacing.
fn projection_dots(
    file: FileId,
    tokens: &mut [Spanned],
    errors: &mut Vec<Diagnostic>,
) {
    for i in 1..tokens.len().saturating_sub(1) {
        if !matches!(&tokens[i].1,Token::Operator(op) if op==".")
            || tokens[i].2 != tokens[i + 1].0
            || !matches!(tokens[i + 1].1, Token::Ident(_))
        {
            continue;
        }
        if tokens[i - 1].2 != tokens[i].0 {
            continue;
        }
        if tokens[i - 1].1 == Token::Left {
            tokens[i].1 = Token::ProjSection;
            if !matches!(
                tokens[i + 2..].iter().find(|(_, t, _)| !matches!(
                    t,
                    Token::LineComment(_) | Token::BlockComment(_)
                )),
                Some((_, Token::Right, _))
            ) {
                errors.push(Diagnostic::error(
                    ErrorCode::ProjectionDot,
                    "a projection section must have the form (.field)",
                    Span::new(file, tokens[i].0, tokens[i].2),
                ));
            }
        } else if matches!(
            tokens[i - 1].1,
            Token::Ident(_)
                | Token::Right
                | Token::BracketClose
                | Token::Close
                | Token::Int(_)
                | Token::Char(_)
                | Token::String(_)
        ) {
            tokens[i].1 = Token::Proj;
        }
    }
}
}

프로젝션 앞에 오는 토큰은 식별자, 리터럴, 닫는 구분자 중 하나여야 합니다. ( 바로 뒤에 오고 변수 식별자 바로 앞에 오는 점은 프로젝션 섹션 토큰입니다. 그 밖의 점은 연산자로 남습니다. 특히 같은 규칙에 따라 x .y는 일반 연산자 출현으로 남습니다.

E0004는 (.x y)처럼 섹션으로 인식되었지만 형식이 잘못된 경우를 진단합니다. 포매터는 점과 레이블 토큰을 붙여 두고, 여기에 붙은 주석은 그 토큰 열 바깥으로 옮깁니다. 공백만 바꾸는 재작성이 자칫 프로그램의 의미를 바꿀 수 있으므로 두 결정 모두 테스트합니다.

필드 메타데이터를 명목적으로 유지하기

각 레코드 생성자는 필드 타입과 함께 필드 레이블을 보관합니다. 데이터 타입 검증은 다른 생성자와 섞인 레코드(E0240)와 선언에서 반복된 레이블(E0104)을 거부합니다. 이름 붙은 생성은 누락된 필드, 중복된 필드, 알 수 없는 필드를 각각 E0241, E0242, E0243으로 검사합니다.

레코드 패턴은 이름 변환 과정에서 위치 기반 생성자 패턴으로 바뀝니다. 리네이머는 각 레이블의 슬롯을 찾고, 생략된 모든 슬롯을 와일드카드로 채웁니다. 따라서 패턴 검사기와 Core 린트에는 새로운 레코드 패턴 규칙이 필요하지 않습니다. 값 환경에는 어떤 필드도 추가되지 않습니다.

필드 의무 풀기

wanted는 수신자 타입, 레이블, 새 필드 타입, 소스 위치, 추론 레벨을 기록합니다. 위치는 하나의 갱신 안에 있는 개별 필드를 구별합니다.

#![allow(unused)]
fn main() {
/// A field site's position distinguishes updates of several named slots.
pub type FieldSite = (u32, usize);

/// Field constraints never enter a polymorphic scheme.
#[derive(Clone)]
struct FieldWanted {
    record: Type,
    label: String,
    field: Type,
    site: FieldSite,
    span: crate::source::Span,
    level: u32,
}
}

프로젝션 추론 분기는 그 의무를 만들고 새 결과 타입을 반환합니다. 이후 해석이 성공하면 그 결과를 인스턴스화된 필드 선언과 단일화합니다:

#![allow(unused)]
fn main() {
Kind::Project(e, label) => {
    let record = self.infer(env, e)?;
    let field = self.fresh();
    self.field(
        record,
        label.clone(),
        field.clone(),
        (expr.node, 0),
        expr,
    )?;
    Ok(field)
}
}

solve_field는 생성자와 필드 슬롯을 식별하거나, 해당 필드가 없는 타입을 보고하거나(E0244), 알 수 없는 수신자를 미해결 상태로 남겨 둡니다. 해결된 필드 하나가 다른 필드의 수신자를 식별할 수 있으므로 이를 둘러싼 루프가 반복됩니다.

#![allow(unused)]
fn main() {
fn solve_fields(
    &mut self,
    complete: Option<u32>,
) -> Result<(), Diagnostic> {
    loop {
        let pending = std::mem::take(&mut self.wanted);
        let count = pending.len();
        for wanted in pending {
            if !self.solve_field(&wanted)? {
                self.wanted.push(wanted);
            }
        }
        if self.wanted.len() == count {
            break;
        }
    }
    if let Some(level) = complete
        && let Some(w) = self.wanted.iter().find(|w| w.level > level)
    {
        let mut error = Diagnostic::error(
            ErrorCode::AmbiguousField,
            format!("ambiguous field access .{}", w.label),
            w.span,
        );
        error.notes.push(
            "add a type annotation to identify the record type".into(),
        );
        return Err(error);
    }
    Ok(())
}
}

시그니처 주도 검사는 람다 본문을 방문하기 전에 람다 매개변수 타입을 공급합니다. 이는 지역 바인딩이 공유 필드에 접근할 때 중요합니다. 바깥 시그니처는 그 지역 그룹이 일반화되기 전에 사용할 수 있어야 합니다. 기존의 경직된 시그니처 검사는 여전히 지나치게 많은 다형성을 주장하는 표기를 거부합니다.

엄격한 생성과 접근 정교화하기

소스에서의 필드 순서는 메모리에서의 필드 순서와 무관합니다. 따라서 생성은 제공된 각 식마다 엄격한 let을 도입한 뒤, 그 변수들을 선언 순서대로 사용해 생성자를 만듭니다.

#![allow(unused)]
fn main() {
Kind::Record(id, fields) => {
    let declaration =
        self.constructors[id.index as usize].clone();
    let arguments = self.typed.instantiations[&expr.node]
        .iter()
        .map(|t| convert(t, variables))
        .collect::<Vec<_>>();
    let mut values = vec![Expr::Lit(Lit::Int(0)); fields.len()];
    let mut locals = vec![];
    for (label, e) in fields {
        let index = declaration
            .labels
            .as_ref()
            .unwrap()
            .iter()
            .position(|n| n == label)
            .unwrap();
        let parameter = Id {
            name: self.sess.fresh_id("$field"),
            ty: instantiate(
                &declaration.fields[index],
                &declaration.parameters,
                &arguments,
            ),
        };
        locals.push(BindGroup {
            recursive: false,
            binds: vec![Bind {
                name: parameter.clone(),
                rhs: Box::new(self.expression(e, variables)),
            }],
        });
        values[index] = Expr::Var(parameter);
    }
    let mut body = Expr::Con(id.clone(), arguments, values);
    for local in locals.into_iter().rev() {
        body = Expr::Let(local, Box::new(body));
    }
    body
}
}

프로젝션은 수신자를 한 번 평가하고, 그 유일한 생성자와 매치한 뒤, 선택된 필드 바인더를 반환합니다. 갱신은 같은 매치를 사용하고, 교체할 식을 소스 순서대로 평가하며, 바뀌지 않은 필드를 원래 슬롯에 둔 채 생성자를 다시 만듭니다. 결과 타입은 수신자 타입과 같으므로, 갱신으로 Box Int를 Box Bool로 바꿀 수는 없습니다.

Core는 11장에서 도입한 구문만 봅니다. 독립된 린트가 그 타입을 검사하고, 인터프리터는 평범한 매치와 생성자를 실행합니다. 값 프린터는 생성자의 레이블을 참조하여 Point {x = 3, y = 4}를 선언 순서대로 출력합니다. 이후 네이티브 프로젝션은 필드 로드가 될 것입니다.

실행해 보기

$ cd crates/ch12_records
$ cargo run -q -- run tests/programs/records/nested.fun --lint
(3,Outer {inner = P {x = 10, y = 4}})
$ cargo run -q -- dump --pass=types tests/programs/records/shared-signature.fun
main : Int
$ cargo test --test records

두 번째 레코드에 x 필드를 주고 프로젝션에서 표기를 제거해 보십시오. 모호성 오류를 보면 필드 이름만으로는 왜 항상 충분하지 않은지 알 수 있습니다. 또 하나의 유용한 실험은 수신자와 갱신되는 필드에 서로 다른 오류를 넣어 보는 것입니다. 최종 필드는 선언 순서대로 저장되지만, 첫 번째 오류는 소스의 평가 순서를 따라야 합니다.

연습 문제

  1. 입문. 다형적인 Box a 레코드를 정의하고 그 프로젝션 섹션의 타입을 추론하십시오.
  2. 중급. 같은 필드 레이블을 가진 레코드를 하나 더 추가하고, 모호성 오류를 관찰한 뒤, 타입 시그니처로 해결하십시오.
  3. 중급. 같은 바인딩 그룹 안에서 이름이 유일한 다른 필드가 앞서 해소되지 않은 접근의 모호성을 어떻게 해소할 수 있는지 설명하십시오.
  4. 고급. 소스의 필드 순서가 선언 순서와 다르고, 첫 번째 실패가 평가 순서를 드러내는 생성식을 작성하십시오.
  5. 고급. 행 다형적(row-polymorphic) 접근을 지원하려면 추론과 일반화의 어떤 부분을 바꿔야 하는지 찾아보십시오. 여기서 그 기능을 추가하지는 마십시오.

더 읽을거리

Haskell Report는 전통적인 레코드 생성, 갱신, 선택자 함수를 설명합니다 (Simon Marlow 2010). GHC 제안 282는 레코드 점 구문의 동기와 그것이 오버로딩된 필드 접근과 맺는 관계를 설명합니다 (Mitchell and Fletcher, n.d.). Pierce는 구조적 레코드 타입 체계를 전개하는데, 이는 Fun의 명목적 선택과 비교해 보기에 유용합니다 (Pierce 2002).

13장: 카인드와 타입 생성자

새 파일

  • src/kinds.rs

변경된 파일

  • src/types.rs
  • src/typecheck.rs
  • src/core_lint.rs
  • src/ast.rs
  • src/core.rs
  • src/core_type.rs
  • src/data.rs
  • src/elaborate.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/parse.rs
  • src/rename.rs

동기

Int는 값을 기술합니다. Maybe Int도 마찬가지입니다. 하지만 Maybe는 그 자체만으로는 값을 기술하기 전에 다른 타입을 하나 더 기다리고 있습니다. 우리는 이미 데이터 타입의 인자 개수를 세는 방식으로 이를 처리하고 있습니다. 그런데 그 인자 중 하나가 그 자체로 Maybe 같은 것이라면 어떻게 될까요?

타입이 f a인 필드를 생각해 봅시다. 여기서 f는 타입을 받아야 하고, a는 그 인자 자리를 채웁니다. 우리에게는 이 둘의 서로 다른 역할을 기술할 방법이 필요합니다. 이러한 기술을 카인드(kind)라고 부릅니다. 카인드는 일반 타입의 풍부한 구성 요소 대신 화살표와 *를 사용하므로, 다행히도 낯익어 보입니다.

우리에게 필요한 장치 중 상당수도 이미 익숙한 것입니다. 타입을 추론했던 것과 마찬가지로, 방정식을 풀어 카인드를 추론할 것입니다. 이는 컴파일러의 한 아이디어가 두 번째로 제 몫을 톡톡히 해내는 좋은 예입니다.

현재의 언어

data Wrap f a = Wrap (f a)

unwrap :: Wrap Maybe Int -> Maybe Int
unwrap (Wrap x) = x

main = unwrap (Wrap (Just 42))

Wrap의 카인드는 (* -> *) -> * -> *입니다. data Fix f = Fix (f (Fix f)) 같은 재귀 선언은 Fix에 (* -> *) -> * 카인드를 부여합니다. 선언 간 의존성은 강하게 연결된 요소(strongly connected component)로 묶입니다. 각 요소를 먼저 추론한 뒤에야 제약되지 않은 카인드 변수가 *로 기본 지정됩니다.

type Pair a = (a,a)

type Count = Int

addPair :: Pair Count -> Count
addPair (x,y) = x + y

main = addPair (20,22)

타입 동의어는 생성자도, 런타임 표현도 도입하지 않습니다. 타입 동의어는 완전히 적용되어야 하며, 부분 적용된 데이터 타입이라면 올바른 카인드를 가질 자리에서도 마찬가지입니다. 순환은 사용되지 않는 순환까지 포함하여 거부됩니다. E0301은 카인드 불일치를, E0302는 부분 적용된 동의어를, E0303은 동의어 순환을 보고합니다. 타입 생성자 [], (,), (,,), (->)는 카인드가 허용하는 곳이라면 적용되지 않은 채로 나타날 수 있습니다.

이론

Maybe :: * -> *는 함수 타입과 비슷하게 읽으십시오. 이것은 값들의 타입 하나를 받아 다른 타입을 만들어 냅니다. 이것을 Int :: *에 적용하면 Maybe Int :: *가 됩니다. 카인드 적용 규칙은 바로 그 단계를 적어 놓은 것입니다.

함수의 인자와 결과는 각각 카인드 *를 가져야 합니다. 생성자 필드 역시 카인드 *를 가져야 합니다. 적용되지 않은 타입 생성자 Maybe에 속하는 값은 있을 수 없기 때문입니다. 카인드와 타입의 구분은 Pierce (Pierce 2002)가 고차 타입 연산자를 제시한 방식을 따릅니다. Fun은 카인드를 추론하지만 타입 수준 람다나 명시적 카인드 표기를 추가하지는 않습니다.

동의어 치환은 Haskell Report의 완전 적용(fully applied) 관례를 따릅니다(Simon Marlow 2010). 동의어에 들어가기 전에 인자를 먼저 전개하므로, 반복된 이름을 선언 순환으로 오인하지 않고 Id (Id Int)를 허용할 수 있습니다. 정의 스택은 전개 중에 실제 재귀를 탐지합니다.

구현

카인드는 타입이 항을 분류하는 것과 같은 방식으로 타입 생성자를 분류합니다. 구현은 카인드 단일화, 발생 검사, 의존성 순서 결정을 결합하여 그러한 분류를 추론합니다.

#![allow(unused)]
fn main() {
pub enum Kind {
    Star,
    Arrow(Box<Kind>, Box<Kind>),
    Meta(u32),
}
}

Store::unify는 해를 따라가고, 화살표 구조를 비교하며, 자신에게 제안된 해 안에 나타나는 메타변수를 거부합니다. finish는 선언 요소의 모든 필드를 검사한 뒤에야 풀리지 않은 셀을 기본값으로 지정합니다.

#![allow(unused)]
fn main() {
pub fn unify(
    &mut self,
    a: &Kind,
    b: &Kind,
    span: Span,
) -> Result<(), Diagnostic> {
    let (a, b) = (self.zonk(a), self.zonk(b));
    match (&a, &b) {
        _ if a == b => Ok(()),
        (Kind::Meta(n), k) | (k, Kind::Meta(n))
            if !self.occurs(*n, k) =>
        {
            self.solutions[*n as usize] = Some(k.clone());
            Ok(())
        }
        (Kind::Arrow(a, b), Kind::Arrow(c, d)) => {
            self.unify(a, c, span)?;
            self.unify(b, d, span)
        }
        _ => Err(Diagnostic::error(
            ErrorCode::Kind,
            format!("cannot unify kind {a} with {b}"),
            span,
        )),
    }
}
}

선언 패스는 먼저 모든 헤드를 등록한 다음, 9장의 Tarjan 구현을 재사용합니다. 시그니처는 그렇게 닫힌 테이블을 기준으로 암묵적 변수의 카인드를 추론합니다. 불변 데이터 타입 환경은 동의어 사용을 검증하고, 전개된 구문을 추론용 타입으로 변환합니다.

이제 타입 저장소는 각 메타변수에 카인드를 기록합니다. 스킴을 인스턴스화할 때는 변수를 새로 만드는 것뿐 아니라 정량화된 카인드도 복사합니다. f a와 Maybe Int의 단일화는 양쪽에서 마지막 적용을 분리하여, 어느 쪽에도 값 타입일 것을 요구하지 않고 f = Maybe와 a = Int를 풀어 냅니다.

Core는 고유한 타입 바인더 식별자를 키로 하는 카인드 테이블을 유지합니다. 린트는 적용, 생성자 인자, 함수 피연산자를 이 테이블에 비추어 독립적으로 검사합니다. 감싸는 어떤 스킴에도 나타나지 않는 내부 타입 변수는 알맞은 카인드의 닫힌 증인(witness)을 받습니다. 이러한 컴파일러 전용 타입 상수에는 값도 소스 구문도 없으며 소거 단계에서 사라집니다. 이는 10장에서 사용한 Int 증인을 Int가 적합하지 않은 카인드로까지 확장한 것입니다.

실행해 보기

$ cd crates/ch13_kinds
$ cargo run -q -- run tests/programs/kinds/wrapper.fun --lint
Just 42
$ cargo run -q -- dump --pass=types tests/programs/kinds/wrapper.fun
main : Maybe Int
$ cargo run -q -- check tests/programs/kinds/synonym-self.fun
E0303

마지막 명령은 실패하며 잘못된 카인드 적용을 가리킵니다. 다음으로 부분 적용된 데이터 타입 생성자를, 그다음에는 부분 적용된 동의어를 시도해 보십시오. 전자는 상위 카인드에서 유용하게 쓰일 수 있지만, 후자는 우리의 동의어 규칙에 의해 거부됩니다. 빠진 인자의 개수가 겉보기에 같다고 해서 두 선언을 서로 바꿔 쓸 수 있는 것은 아닙니다.

연습 문제

  1. 준비 운동. data Compose f g a = Compose (f (g a))의 카인드를 추론하십시오.
  2. 중급. data Bad f = Bad (f f)가 발생 검사에서 실패하는 이유를 설명하십시오.
  3. 중급. 서로 의존하는 선언들에 대한 카인드 추론 과정을 추적하십시오.
  4. 고급. 정교화 과정에서 카인드 * -> *의 내부 증인이 필요한 항을 찾고, 그에 대응하는 Core 적용을 살펴보십시오.

더 읽을거리

14장: 타입 클래스

새 파일

  • src/classes.rs
  • src/evidence.rs
  • src/elaborate/dictionaries.rs
  • src/primitives.rs

변경된 파일

  • src/typecheck.rs
  • src/elaborate.rs
  • src/types.rs
  • src/ast.rs
  • src/core.rs
  • src/core_lint.rs
  • src/core_type.rs
  • src/data.rs
  • src/desugar.rs
  • src/eval.rs
  • src/fixity.rs
  • src/kinds.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/parse.rs
  • src/rename.rs
  • src/renamed.rs
  • src/syn.rs

동기

두 리스트는 어떻게 비교할까요? 리스트의 원소들을 비교하므로, 그 원소들을 비교하는 방법을 알아야 합니다. 모든 리스트 비교에 동등성 함수를 넘겨줄 수도 있습니다. 그렇게 해도 동작하지만, 머지않아 똑같은 몇 가지 연산을 이리저리 넘겨주는 데 많은 시간을 쓰게 될 것입니다.

타입 클래스를 사용하면 그런 배관 작업을 컴파일러에게 맡길 수 있습니다. 우리는 클래스에 연산을 기술하고, 인스턴스에서 그 구현을 제공합니다. 컴파일러는 각 호출에 어떤 구현이 필요한지 알아내어 그 구현을 넘겨줍니다.

멋진 점은 정체를 알 수 없는 새로운 런타임 기능이 필요하지 않다는 것입니다. 클래스는 함수들의 레코드가 되며, 이 레코드를 흔히 딕셔너리라고 부릅니다. 그 인자들을 명시적으로 드러내고 나면, 기존의 Core 인터프리터로 결과를 실행할 수 있습니다. 이 장에서는 그 변환 과정을 처음부터 끝까지 따라가 보겠습니다.

현재의 언어

클래스를 하나 정의하고 정수에 대한 인스턴스를 만들어 봅시다.

class Value a where
  value :: a -> Int

instance Value Int where
  value x = x + 1

main = value 41

클래스 매개변수는 고차 카인드를 가질 수 있습니다. unwrap :: f a -> a라는 메서드가 있으면 클래스 매개변수 f의 카인드는 * -> *가 됩니다. 나머지 변수 a는 독립적으로 다형적입니다. 클래스는 슈퍼클래스와 기본 메서드 본문을 가질 수 있습니다. 인스턴스 메서드에는 일반적인 등식, 가드, 지역 where 바인딩을 쓸 수 있습니다.

이제 Prelude는 Eq, Ord, Show, Num, Fractional, Semigroup, Monoid를 정의합니다. 산술 연산과 비교 연산은 메서드로 해석됩니다. 리터럴은 여전히 단형적입니다. 1의 타입은 Int이고, 1.0의 타입은 Double입니다. 따라서 1 + 2.0은 실패합니다. toDouble 1 + 2.0은 성공합니다. 기본 타입 지정(defaulting)은 없습니다.

same :: Ord a => a -> a -> Bool
same x y = x == y

main = same [1,2] [1,2]

Ord가 Eq 슈퍼클래스를 제공하므로 시그니처만으로 충분한 증거가 주어집니다. 인스턴스가 없으면 E0210을 보고합니다. 형식이 잘못되었거나 겹치는 인스턴스, 그리고 누락된 메서드는 E0211을 보고합니다. 결과 타입에 나타나지 않는 변수를 가진 제약은 E0230을 보고합니다. 예를 들어 show Nothing은 원소의 Show 인스턴스를 고를 수 없습니다. 명시적인 Maybe Int 표기를 붙이면 그 모호성이 해소됩니다.

Double은 부동소수점 산술, 비교, 변환, 수학 함수를 추가합니다. 프리미티브 이름은 내장된 Prelude 안에서만 쓰이도록 비공개로 유지됩니다. 소스 언어에는 여전히 IO가 없으므로 run은 main의 값을 출력합니다. 이 장에서 ++는 여전히 문자열을 이어 붙입니다. 리스트에 대한 ++는 완전한 Prelude 장에서 다룹니다. <>는 이미 문자열과 리스트를 모두 지원합니다.

이론

타입 Eq a => a -> a -> Bool은 누군가 a에 대한 동등성을 제공하면 a 타입의 두 값을 비교할 수 있다는 뜻입니다. => 앞부분을 문맥(context)이라고 합니다. 이 타입을 가진 함수를 사용하면 추론 과정에서 그 동등성 연산에 대한 요청이 만들어집니다. 우리는 이것을 요구(wanted) 제약이라고 부릅니다.

어떤 요청은 Int에 대한 인스턴스처럼 바로 답을 얻습니다. 나머지는 타입 변수에 관한 요청으로 남습니다. 바인딩을 일반화할 때 이런 요청은 그 바인딩의 스킴에 포함될 수 있으며, 그러면 호출자가 답을 제공합니다. 반면 바깥 변수가 관련된 제약은 둘러싼 스코프에 속합니다. Jones는 이러한 연산을 실행 가능한 형태로 설명합니다 (Jones 1999).

인스턴스를 찾는 것은 일의 절반에 불과합니다. 런타임에 전달할 무언가도 필요합니다. 그것이 제약에 대한 증거(evidence)입니다. Ord a 사전에는 이미 Eq a 슈퍼클래스 사전이 들어 있습니다. Eq a가 주어지면 리스트 인스턴스는 Eq [a]에 대한 사전을 만들 수 있습니다. 이 단계들을 따라가면 사전 변수, 필드 선택, 인스턴스 적용으로 이루어진 평범한 식이 만들어집니다. 런타임에 수행할 탐색은 남지 않습니다.

시그니처는 경직된(rigid) 스콜렘으로 검사하며, 시그니처의 문맥은 주어진(given) 제약으로 취급합니다. 시그니처가 있는 바인딩은 자신의 본문 안에서 다형적으로 사용될 수 있습니다. Haskell Report(Simon Marlow 2010)를 따라, 타입 추론 의존성에서는 시그니처가 있는 바인딩으로 향하는 간선을 생략합니다. 재귀 함수에는 여전히 공유된 재귀 환경이 필요하므로 런타임 의존성에서는 그 간선을 유지합니다. 이 둘은 같은 소스 의존성 그래프를 서로 다르게 사용하는 것입니다.

일반화는 여전히 제한 없이 이루어집니다. comparison = (==) 같은 제약이 있는 값은 사전을 인자로 받는 함수로 정교화됩니다. 그 본문은 사전이 제공될 때 평가되므로 소스 수준의 공유가 사라집니다. Fun은 사전 전달에서 비롯된 이 결과를 받아들입니다. 순수성 덕분에 이것은 건전합니다.

구현

classes.rs는 슈퍼클래스 탐색, 인스턴스 매칭, 함의를 구현합니다. 주 추론기는 계속해서 레벨 기반의 가변 메타변수를 사용합니다. 인스턴스 헤드를 매칭하는 데 쓰는 작은 불변 맵은 그 조회 안에서만 쓰입니다. 이것은 치환 기반의 또 다른 타입 검사기가 아닙니다.

#![allow(unused)]
fn main() {
pub enum Evidence {
    Given(usize, Vec<usize>),
    Instance(usize, Vec<Type>, Vec<Evidence>),
}
}

주어진 제약(given)은 어휘적 사전 환경 안에서의 자기 위치와 슈퍼클래스 필드를 거쳐 가는 경로를 저장합니다. 인스턴스 증명은 선택된 인스턴스, 그 타입 인자, 그리고 그 전제 조건들의 증명을 저장합니다.

#![allow(unused)]
fn main() {
pub fn resolve(
    &self,
    goal: &Predicate,
    givens: &[Predicate],
    span: Span,
) -> Result<Evidence, Diagnostic> {
    for (i, given) in givens.iter().enumerate() {
        if given.ty == goal.ty
            && let Some(path) =
                self.super_path(&given.class, &goal.class)
        {
            return Ok(Evidence::Given(i, path));
        }
    }
    let (i, args, context) =
        self.by_instance(goal).ok_or_else(|| {
            error(
                ErrorCode::NoInstance,
                format!("no instance for {} {}", goal.class, goal.ty),
                span,
            )
        })?;
    let proofs = context
        .iter()
        .map(|p| self.resolve(p, givens, span))
        .collect::<Result<Vec<_>, _>>()?;
    Ok(Evidence::Instance(i, args, proofs))
}
}

클래스 검증은 슈퍼클래스 순환, 메서드 소속, 메서드의 추가 제약, 명목 인스턴스 헤드 안의 서로 다른 변수, 중첩, 슈퍼클래스 가용성을 검사합니다. 카인드 추론은 제약되지 않은 클래스 카인드에 기본값을 정하기 전에 슈퍼클래스 의존성부터 처리합니다. 인스턴스 문맥은 헤드의 변수만 언급할 수 있습니다. 따라서 풀이는 구조적으로 더 작은 타입으로 내려갑니다.

일반화 시점에 필드 제약은 여전히 클래스 제약보다 먼저 실행됩니다. zonking 이후 evidence.rs는 어휘적 given을 사용할 수 있는 동안 변수 출현 위치에 클래스 증명을 기록합니다. 정교화(elaboration)는 이렇게 검사된 증명을 소비합니다.

#![allow(unused)]
fn main() {
pub struct EvVar {
    pub node: u32,
    pub index: usize,
}
}

elaborate/dictionaries.rs는 클래스마다 명목적 Core 사전 타입을 하나씩 선언합니다. 그 필드에는 슈퍼클래스 사전이 먼저 오고 그 뒤에 메서드가 옵니다. 메서드 지역 한정자는 필드 타입 안에서 System F 다형성이 됩니다. 이것이 소스 언어에 rank-N 시그니처를 추가하는 것은 아닙니다. 각 인스턴스는 자신의 타입 변수와 문맥 사전에 대해 추상화된 전역 값이 됩니다. 메서드 선택자는 사전 생성자에 대한 평범한 매치입니다.

#![allow(unused)]
fn main() {
pub(super) fn dictionary_type(&self, class: &str, ty: Type) -> Type {
    let c = &self.constructors[self.dicts[class]];
    instantiate(&c.result, &c.parameters, &[ty])
}
}

기본 구현에는 이름 붙은 전역이 주어지며, 누락된 각 구현은 해당 인스턴스에 맞게 특수화된 기본 본문의 복사본으로부터 검사됩니다. 이렇게 하면 엄격한 사전 생성이 재귀적인 값의 매듭을 묶지 않게 됩니다. 사전이 도입한 의존성은 평가 전에 정렬됩니다. 함수가 아닌 값의 재귀적 초기화는 거부됩니다. 평가기는 값을 평가하기 전에 함수 클로저를 설치하며, 제약이 없는 각 최상위 값은 여전히 한 번만 평가됩니다.

primitives.rs는 고정된 원시 연산 시그니처와 구현을 제공합니다. 정수 연산은 63비트 래핑을 유지합니다. 부동소수점 표시는 지정된 위치 표기 범위, 부호 있는 지수, 명시적인 특수 값과 함께 최단 왕복(round-trip) 자릿수를 사용합니다. 테스트는 음의 0, 경계 크기, 유니코드 문자열, 실패한 변환을 다룹니다. 공유 런타임 장에서는 사소하지 않은 연산들을 fun-rt로 옮길 것입니다.

실행해 보기

$ cd crates/ch14_classes
$ cargo run -q -- run tests/programs/classes/minimal.fun --lint
42
$ cargo run -q -- dump --pass=types tests/programs/classes/principal-type.fun
main : Num a => a -> a -> a
$ cargo run -q -- check tests/programs/classes/mixed-literals.fun
E0201

실패하는 명령은 레이블이 붙은 진단도 함께 출력합니다. --dump=core는 $D 사전 타입, $f 인스턴스 전역, 명시적인 사전 적용을 드러냅니다. 작은 사전 스냅숏들은 예제를 읽기 쉽게 하려고 Prelude를 끕니다. 일반 빌드는 사용자 스코프보다 먼저 내장된 Prelude를 컴파일합니다. Prelude 이름을 가려도 Prelude 자체의 참조는 바뀌지 않습니다.

식 타입 표기는 감싸는 괄호 없이도 동작합니다. 다만 주의해야 할 곳이 한 군데 있습니다. case 가드 안에서는 타입 표기를 괄호로 감싸서, 그 타입 화살표가 대안의 본문으로 이어지는 화살표와 혼동되지 않게 하십시오.

연습 문제

  1. 준비 운동. [Maybe Int]에 대한 동등성 비교의 사전 인자를 살펴보십시오.
  2. 중급. 슈퍼클래스에 비해 문맥이 불충분한 인스턴스를 작성한 다음, 빠진 증명을 찾아내십시오.
  3. 중급. show Nothing은 모호하지만 show (Nothing :: Maybe Int)는 그렇지 않은 이유를 설명하십시오.
  4. 고급. 다형 재귀 픽스처의 타입 추론 의존성 컴포넌트와 런타임 의존성 컴포넌트를 비교하십시오.
  5. 심화. 슈퍼클래스 사전의 Core 타입을 손상시키고, 표면 추론을 거치지 않는 린트 회귀 테스트를 추가하십시오.

더 읽을거리

15장: 모나드, do 표기법, IO

새 파일

  • src/io_lower.rs

변경된 파일

  • src/desugar.rs
  • src/eval.rs
  • src/typecheck.rs
  • src/ast.rs
  • src/data.rs
  • src/elaborate.rs
  • src/fixity.rs
  • src/kinds.rs
  • src/layout.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/primitives.rs
  • src/rename.rs
  • src/session.rs

동기

지금까지 우리 프로그램은 바깥 세계와 꽤 제한된 관계만 맺어 왔습니다. 드라이버는 main을 평가하고, 답을 출력한 뒤, 퇴근합니다. 질문을 던지고, 응답을 읽고, 다음에 무엇을 할지 결정할 수 있는 프로그램을 작성해 봅시다.

우리는 일반 표현식을 순수하게 유지하고 싶으므로, IO 값을 평가해도 그 효과는 수행되지 않습니다. 우리는 동작(action)을 값으로 표현하고, 명시적인 경계에서 그것을 실행할 것입니다. 이 구분 덕분에 우리가 이미 알고 있는 함수들을 사용해 동작을 만들고 조합할 수 있습니다.

14장의 타입 클래스는 이러한 조합을 기술하는 방법을 제공합니다. Functor, Applicative, Monad는 IO와 함께 동작하지만, Maybe, Either, 리스트에 대한 유용한 연산도 기술합니다. 우리는 먼저 이 연산들이 무엇을 하는지 살펴보고, 구문을 손으로 직접 전개해 보겠습니다. 용어에 신비로움을 덧씌우지 않아도 여기에는 이해할 것이 충분히 많습니다.

현재의 언어

이제 완전한 프로그램은 그 시그니처를 직접 작성했든 추론되었든 main :: IO ()를 가집니다. 가장 작은 예제는 새 Prelude 프린터를 호출합니다:

main = putStrLn "hello"

do 표기법은 일반 데이터에도 쓸 수 있습니다. 여기서는 두 번의 바인드가 Just에서 값을 꺼내고, pure가 같은 Maybe 인스턴스 안에서 결과를 만듭니다:

main = print (do { x <- Just 20; y <- Just 22; pure (x+y) })

블록은 반드시 식으로 끝나야 합니다. x <- action 문은 남은 문에서만 x를 바인딩합니다. let 문은 in 없이 지역 선언을 도입합니다. 패턴은 중첩될 수 있으며, 여기에는 튜플, 리스트, as-패턴, 명목적 레코드 패턴이 포함됩니다. 반박 가능한 패턴이 매칭에 실패하면 그 소스 위치와 함께 error를 호출합니다. Fun에는 MonadFail이 없으므로, 둘러싼 모나드가 Maybe일 때에도 실패는 런타임 오류가 됩니다.

putStr, putStrLn, getLine, print, exitWith는 프로그램의 외부 인터페이스를 제공합니다. mapM, mapM_, sequence_, when, unless는 흔히 쓰이는 순서화 패턴을 표현합니다. 진입점이 없거나 타입이 잘못되었으면 E0250을 보고합니다. 본문이 명시적 시그니처를 위반하면 그 대신 시그니처 검사 중에 E0203으로 실패할 수 있습니다.

이론

어떤 액션이 a를 만들어 내고, 프로그램의 나머지 부분은 그 a를 써서 다음 액션을 만드는 법을 안다고 가정해 봅시다. 바인드는 이 둘을 연결합니다. 두 번째 인자가 함수인 까닭은 다음 액션이 첫 번째 결과에 따라 달라질 수 있기 때문입니다. 이 타입을 왼쪽에서 오른쪽으로 천천히 읽어 보십시오 (Jones 1993).

IO의 경우, 소스 검사기는 IO를 추상적인 명목 생성자로 취급합니다. Core는 액션을 unit에서 그 결과로 가는 함수로 표현합니다:

unit 인자는 지연을 담아 둘 자리를 마련해 줍니다. 함수를 만든다고 해서 그 본문이 실행되지는 않습니다. returnIO는 저장해 둔 값을 반환하는 함수를 만들고, bindIO는 첫 번째 액션을 실행한 뒤 그 결과를 다음 액션에 넘기는 함수를 만듭니다. 부수 효과를 일으키는 기본 연산들은 그 본문 안에 들어 있습니다.

이 표현은 신뢰할 수 있는 Prelude와 컴파일러만 사용할 수 있습니다. 소스 프로그램에는 추상적인 IO a 값만 보이므로, 액션에 ()를 적용해 아무 곳에서나 실행할 수 없습니다. 바로 이 경계 덕분에 일반적인 Fun 식이 순수하게 유지됩니다.

엄격성은 여전히 적용됩니다. putStrLn "hello"를 만들면 그 문자열 인자를 평가하고 액션을 반환합니다. 출력은 하지 않습니다. error "bad"를 즉시 요구되는 인자로 넘겨 액션을 만들면, 액션이 실행되기 전에 실패할 수 있습니다. 순서화가 제어하는 것은 효과이지, 임의의 순수한 인자의 평가가 아닙니다. Haskell Report는 이와 유사한 do 변환을 기술하지만 (Simon Marlow 2010), 그 지연 평가를 Fun의 연산적 설명에 들여와서는 안 됩니다.

모나드 법칙은 인스턴스가 따라야 할 동작을 기술합니다. 컴파일러는 타입을 검사할 뿐, 그 등식들을 검사하지는 않습니다. 특히 비종료가 관련된 등식은 엄격한 언어에서 주의해서 다뤄야 합니다. 메커니즘은 여전히 14장의 사전 전달입니다. 평가기에 새로운 클래스 디스패치는 나타나지 않습니다.

구현

파서는 어휘적 이름 변경 전에 desugar::do_block을 호출합니다. 문장을 뒤에서부터 처리하면 남은 블록이 각 바인드의 컨티뉴에이션이 됩니다. 변수 패턴은 곧바로 람다가 됩니다. 반박 가능한 패턴은 실패 대안을 가진 case를 도입합니다. 이때 쓰는 임시 이름은 소스 식별자로 작성할 수 없습니다.

#![allow(unused)]
fn main() {
use crate::{
    data::{ConId, DataEnv},
    diagnostics::{Diagnostic, ErrorCode},
    renamed::{self, BindGroup, Expr, Kind, Pat, PatKind, Row, RowBody},
    session::{Session, Stop},
    source::Span,
    syn,
};
struct Desugar<'a> {
    sess: &'a mut Session,
    data: &'a DataEnv,
}
impl Desugar<'_> {
    fn node(&mut self, kind: Kind, span: Span) -> Expr {
        Expr {
            kind,
            span,
            node: self.sess.fresh_node(),
        }
    }
    fn constructor(&mut self, name: &str, span: Span) -> ConId {
        if let Some(info) = self.data.constructor(name) {
            return info.id.clone();
        }
        self.sess.emit(Diagnostic::error(
            ErrorCode::UnboundConstructor,
            format!("sugar requires constructor {name} from the Prelude"),
            span,
        ));
        ConId {
            index: 0,
            name: name.into(),
        }
    }
    fn tuple(&mut self, n: usize, span: Span) -> ConId {
        if !matches!(n, 0 | 2 | 3) {
            self.sess.emit(Diagnostic::error(
                ErrorCode::Parse,
                "only unit, pairs, and triples are supported",
                span,
            ));
        }
        self.constructor(
            &format!("({})", ",".repeat(n.saturating_sub(1))),
            span,
        )
    }
    fn pattern(&mut self, p: Pat) -> Pat {
        let span = p.span;
        let kind = match p.kind {
            PatKind::List(ps) => {
                let nil = self.constructor("[]", span);
                let cons = self.constructor(":", span);
                let mut tail = Pat {
                    kind: PatKind::Con(nil, vec![]),
                    span,
                };
                for p in ps.into_iter().rev() {
                    tail = Pat {
                        kind: PatKind::Con(
                            cons.clone(),
                            vec![self.pattern(p), tail],
                        ),
                        span,
                    };
                }
                return tail;
            }
            PatKind::Tuple(ps) => PatKind::Con(
                self.tuple(ps.len(), span),
                ps.into_iter().map(|p| self.pattern(p)).collect(),
            ),
            PatKind::Con(id, ps) => PatKind::Con(
                id,
                ps.into_iter().map(|p| self.pattern(p)).collect(),
            ),
            PatKind::As(id, p) => {
                PatKind::As(id, Box::new(self.pattern(*p)))
            }
            PatKind::Int(n) => PatKind::Int((n << 1) >> 1),
            other => other,
        };
        Pat { kind, span }
    }
    fn groups(&mut self, groups: Vec<BindGroup>) -> Vec<BindGroup> {
        groups
            .into_iter()
            .map(|mut g| {
                for b in &mut g.binds {
                    b.rhs = self.expression(b.rhs.clone());
                }
                g
            })
            .collect()
    }
    fn row(&mut self, row: Row) -> Row {
        let body = match row.body {
            RowBody::Plain(e) => {
                RowBody::Plain(Box::new(self.expression(*e)))
            }
            RowBody::Guarded(gs) => RowBody::Guarded(
                gs.into_iter()
                    .map(|(g, e)| (self.expression(g), self.expression(e)))
                    .collect(),
            ),
        };
        Row {
            patterns: row
                .patterns
                .into_iter()
                .map(|p| self.pattern(p))
                .collect(),
            body,
            locals: self.groups(row.locals),
            span: row.span,
        }
    }
    fn equations(&mut self, rows: Vec<Row>, span: Span) -> Expr {
        let mut rows: Vec<_> =
            rows.into_iter().map(|r| self.row(r)).collect();
        if rows.len() == 1
            && rows[0]
                .patterns
                .iter()
                .all(|p| matches!(p.kind, PatKind::Var(_)))
            && matches!(rows[0].body, RowBody::Plain(_))
        {
            let row = rows.pop().unwrap();
            let RowBody::Plain(body) = row.body else {
                unreachable!()
            };
            let mut body = *body;
            if !row.locals.is_empty() {
                body =
                    self.node(Kind::Let(row.locals, Box::new(body)), span);
            }
            for p in row.patterns.into_iter().rev() {
                let PatKind::Var(id) = p.kind else {
                    unreachable!()
                };
                body = self.node(Kind::Lam(id, Box::new(body)), span);
            }
            return body;
        }
        // ANCHOR: equations-match
        let params: Vec<_> = (0..rows[0].patterns.len())
            .map(|_| self.sess.fresh_id("$match"))
            .collect();
        let scruts = params
            .iter()
            .map(|id| self.node(Kind::Var(id.clone()), span))
            .collect();
        let mut body = self.node(Kind::Match(scruts, rows), span);
        for id in params.into_iter().rev() {
            body = self.node(Kind::Lam(id, Box::new(body)), span);
        }
        body
        // ANCHOR_END: equations-match
    }
    fn expression(&mut self, expr: Expr) -> Expr {
        let mut work = vec![(Some(expr), None)];
        let mut values: Vec<Expr> = Vec::new();
        while let Some((expr, saved)) = work.pop() {
            match expr {
                Some(Expr {
                    kind: Kind::App(f, a),
                    node,
                    span,
                }) => {
                    work.push((None, Some((node, span))));
                    work.push((Some(*a), None));
                    work.push((Some(*f), None));
                }
                Some(expr) => values.push(self.expression_leaf(expr)),
                None => {
                    let a = values.pop().expect("application argument");
                    let f = values.pop().expect("application function");
                    let (node, span) = saved.expect("application metadata");
                    values.push(Expr {
                        node,
                        span,
                        kind: Kind::App(Box::new(f), Box::new(a)),
                    });
                }
            }
        }
        values.pop().expect("desugared root")
    }
    fn expression_leaf(&mut self, expr: Expr) -> Expr {
        let span = expr.span;
        let kind = match expr.kind {
            Kind::Record(id, fields) => Kind::Record(
                id,
                fields
                    .into_iter()
                    .map(|(n, e)| (n, self.expression(e)))
                    .collect(),
            ),
            Kind::Project(e, n) => {
                Kind::Project(Box::new(self.expression(*e)), n)
            }
            Kind::Update(e, fields) => Kind::Update(
                Box::new(self.expression(*e)),
                fields
                    .into_iter()
                    .map(|(n, e)| (n, self.expression(e)))
                    .collect(),
            ),
            Kind::Annotated(e, t) => {
                Kind::Annotated(Box::new(self.expression(*e)), t)
            }
            Kind::Bool(b) => Kind::Con(
                self.constructor(if b { "True" } else { "False" }, span),
            ),
            Kind::Equations(rows) => return self.equations(rows, span),
            Kind::PatternLam(patterns, body) => {
                return self.equations(
                    vec![Row {
                        patterns,
                        body: RowBody::Plain(body),
                        locals: vec![],
                        span,
                    }],
                    span,
                );
            }
            Kind::Case(e, rows) => Kind::Match(
                vec![self.expression(*e)],
                rows.into_iter().map(|r| self.row(r)).collect(),
            ),
            Kind::If(c, a, b) => {
                let c = self.expression(*c);
                let a = self.expression(*a);
                let b = self.expression(*b);
                let rows = [("True", a), ("False", b)]
                    .into_iter()
                    .map(|(name, body)| Row {
                        patterns: vec![Pat {
                            kind: PatKind::Con(
                                self.constructor(name, span),
                                vec![],
                            ),
                            span,
                        }],
                        body: RowBody::Plain(Box::new(body)),
                        locals: vec![],
                        span,
                    })
                    .collect();
                Kind::Match(vec![c], rows)
            }
            Kind::List(es) => {
                let nil = self.constructor("[]", span);
                let cons = self.constructor(":", span);
                let mut tail = self.node(Kind::Con(nil), span);
                for e in es.into_iter().rev() {
                    let con = self.node(Kind::Con(cons.clone()), span);
                    let head = self.expression(e);
                    let app = self.node(
                        Kind::App(Box::new(con), Box::new(head)),
                        span,
                    );
                    tail = self.node(
                        Kind::App(Box::new(app), Box::new(tail)),
                        span,
                    );
                }
                return tail;
            }
            Kind::Tuple(es) => {
                let id = self.tuple(es.len(), span);
                let mut value = self.node(Kind::Con(id), span);
                for e in es {
                    let e = self.expression(e);
                    value = self.node(
                        Kind::App(Box::new(value), Box::new(e)),
                        span,
                    );
                }
                return value;
            }
            Kind::Lam(id, b) => {
                Kind::Lam(id, Box::new(self.expression(*b)))
            }
            Kind::App(a, b) => Kind::App(
                Box::new(self.expression(*a)),
                Box::new(self.expression(*b)),
            ),
            Kind::Let(gs, b) => {
                Kind::Let(self.groups(gs), Box::new(self.expression(*b)))
            }
            Kind::Neg(a) => Kind::Neg(Box::new(self.expression(*a))),
            Kind::Binary(op, a, b) => Kind::Binary(
                op,
                Box::new(self.expression(*a)),
                Box::new(self.expression(*b)),
            ),
            Kind::Match(..) => {
                unreachable!("Match is produced by this pass")
            }
            other => other,
        };
        Expr {
            kind,
            span,
            node: expr.node,
        }
    }
}
/// Produce Syn after names are resolved, preserving row scopes and guard order.
pub fn desugar(
    sess: &mut Session,
    program: renamed::Program,
) -> Result<syn::Program, Stop> {
    let groups = {
        let mut pass = Desugar {
            sess,
            data: &program.data,
        };
        pass.groups(program.groups)
    };
    sess.finish(syn::Program {
        classes: program.classes,
        data: program.data,
        groups,
        primitives: program.primitives,
        main: program.main,
        span: program.span,
    })
}
/// Statements are lowered before name resolution so ordinary lexical scopes apply.
pub enum Statement {
    Expr(crate::ast::Expr),
    Bind(crate::ast::Pat, crate::ast::Expr),
    Let(Vec<crate::ast::Binding>),
}
pub fn do_pattern(
    e: crate::ast::Expr,
) -> Result<crate::ast::Pat, Diagnostic> {
    use crate::ast::{Kind as K, Pat as P, PatKind as PK};
    let span = e.span;
    let kind = match e.kind {
        K::Chain(a, pairs, None) if pairs.is_empty() => {
            return do_pattern(*a);
        }
        K::Chain(a, pairs, None)
            if pairs.iter().all(|(op, _)| op == ":") =>
        {
            let mut elements = vec![do_pattern(*a)?];
            for (_, e) in pairs {
                elements.push(do_pattern(e)?);
            }
            let mut tail = elements.pop().unwrap();
            for head in elements.into_iter().rev() {
                tail = P {
                    kind: PK::Con(":".into(), vec![head, tail]),
                    span,
                };
            }
            return Ok(tail);
        }
        K::PatternAs(n, p) => PK::As(n, Box::new(do_pattern(*p)?)),
        K::Paren(a) => return do_pattern(*a),
        K::Var(n) if n == "_" => PK::Wild,
        K::Var(n) if n.starts_with(char::is_uppercase) => {
            PK::Con(n, vec![])
        }
        K::Var(n) => PK::Var(n),
        K::App(a, b) => {
            let mut p = do_pattern(*a)?;
            if let PK::Con(_, ps) = &mut p.kind {
                ps.push(do_pattern(*b)?);
                return Ok(p);
            } else {
                return Err(Diagnostic::error(
                    ErrorCode::Parse,
                    "invalid do pattern",
                    span,
                ));
            }
        }
        K::Int(n) => PK::Int(n),
        K::Double(n) => PK::Double(n),
        K::Char(c) => PK::Char(c),
        K::String(s) => PK::String(s),
        K::Neg(a) => {
            if let K::Int(n) = a.kind {
                PK::Int(-n)
            } else {
                return Err(Diagnostic::error(
                    ErrorCode::Parse,
                    "invalid negative pattern",
                    span,
                ));
            }
        }
        K::List(es) => PK::List(
            es.into_iter().map(do_pattern).collect::<Result<_, _>>()?,
        ),
        K::Tuple(es) => PK::Tuple(
            es.into_iter().map(do_pattern).collect::<Result<_, _>>()?,
        ),
        K::Update(a, fs) => {
            fn record(
                mut p: P,
                fields: Vec<(String, P)>,
            ) -> Result<P, Diagnostic> {
                p.kind = match p.kind {
                    PK::As(n, p) => {
                        PK::As(n, Box::new(record(*p, fields)?))
                    }
                    PK::Con(n, ps) if ps.is_empty() => {
                        PK::Record(n, fields)
                    }
                    _ => {
                        return Err(Diagnostic::error(
                            ErrorCode::Parse,
                            "invalid record pattern",
                            p.span,
                        ));
                    }
                };
                Ok(p)
            }
            let fields = fs
                .into_iter()
                .map(|(n, e)| Ok((n, do_pattern(e)?)))
                .collect::<Result<_, Diagnostic>>()?;
            return record(do_pattern(*a)?, fields);
        }
        K::Record(n, fs) => PK::Record(
            n,
            fs.into_iter()
                .map(|(n, e)| Ok((n, do_pattern(e)?)))
                .collect::<Result<_, Diagnostic>>()?,
        ),
        _ => {
            return Err(Diagnostic::error(
                ErrorCode::Parse,
                "invalid do pattern",
                span,
            ));
        }
    };
    Ok(P { kind, span })
}
/// Expand do syntax into ordinary applications, matches, and local bindings.
pub fn do_block(
    statements: Vec<Statement>,
    span: Span,
) -> Result<crate::ast::Expr, Diagnostic> {
    use crate::ast::{
        Expr as E, Kind as K, Pat as P, PatKind as PK, Row as R,
        RowBody as B,
    };
    let node = |kind| E { kind, span };
    let app = |a, b| node(K::App(Box::new(a), Box::new(b)));
    let mut statements = statements;
    let Some(Statement::Expr(mut body)) = statements.pop() else {
        return Err(Diagnostic::error(
            ErrorCode::Parse,
            "do must end with an expression",
            span,
        ));
    };
    for statement in statements.into_iter().rev() {
        body = match statement {
            Statement::Expr(e) => {
                app(app(node(K::Var(">>".into())), e), body)
            }
            Statement::Let(bs) => node(K::LetGroup(bs, Box::new(body))),
            Statement::Bind(p, e) => {
                let continuation = match &p.kind {
                    PK::Var(n) => node(K::Lam(n.clone(), Box::new(body))),
                    PK::Wild => node(K::Lam("$do".into(), Box::new(body))),
                    _ => {
                        let site = p.span;
                        let rows = vec![
                            R {
                                patterns: vec![p],
                                body: B::Plain(Box::new(body)),
                                locals: vec![],
                                span: site,
                            },
                            R {
                                patterns: vec![P {
                                    kind: PK::Wild,
                                    span: site,
                                }],
                                body: B::Plain(Box::new(E {
                                    kind: K::DoFail,
                                    span: site,
                                })),
                                locals: vec![],
                                span: site,
                            },
                        ];
                        node(K::Lam(
                            "$do".into(),
                            Box::new(node(K::Case(
                                Box::new(node(K::Var("$do".into()))),
                                rows,
                            ))),
                        ))
                    }
                };
                app(app(node(K::Var(">>=".into())), e), continuation)
            }
        };
    }
    Ok(body)
}
}

이 배치는 이름 변경 이후에 별도로 수행하는 디슈거링 패스와 다릅니다. 생성된 바인딩도 손으로 작성한 바인딩과 같은 스코프 검사기를 거칩니다. 보조 함수 do_pattern은 <- 왼쪽에 있는 패턴 모양의 구문을 검증합니다. 스팬을 지닌 DoFail 노드는 나중에 파일, 줄, 열 정보를 담은 일반적인 오류 호출로 변환됩니다. 어떤 do 노드도 Core에 도달하지 않습니다.

검사기가 IO 구현을 unit을 받는 함수와 연관 짓게 해 주는 것은 Prelude의 신뢰된 FileId뿐입니다. 사용자에게 보이는 타입 스킴은 명목적으로 유지됩니다. 정교화(elaboration) 후 io_lower::lower는 린트 전에 모든 Core 타입을 순회합니다. 바인더 타입, 타입 적용, 생성자 필드, match 결과 타입, 사전 타입이 그 대상입니다. 가장 바깥 수준의 IO a만 대체한다면 Monad IO 사전 같은 고계 종류 사용을 놓치게 됩니다. 이 순회는 적용되지 않은 생성자 IO도 부분 적용된 함수 생성자 (->) ()로 대체합니다.

실행기는 먼저 함수 클로저를 설치하고 엄격한 최상위 값을 의존성 순서대로 평가한 다음, 진입점을 unit에 적용합니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::Op,
    core::{self, Bind, BindGroup, Expr, Id, Lit},
};
use std::{cell::RefCell, rc::Rc};
/// Language termination is separate from an ordinary runtime failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Failure {
    Runtime(String),
    Exit(i32),
}
impl From<String> for Failure {
    fn from(s: String) -> Self {
        Self::Runtime(s)
    }
}
impl From<&str> for Failure {
    fn from(s: &str) -> Self {
        Self::Runtime(s.into())
    }
}
impl std::fmt::Display for Failure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Runtime(s) => write!(f, "{s}"),
            Self::Exit(n) => write!(f, "exit {n}"),
        }
    }
}
type Env = Vec<(Id, Value)>;
#[derive(Clone, Debug)]
/// A closure captures outer declarations and an optional immutable recursive-group recipe.
pub enum Value {
    Lit(Lit),
    Con(crate::data::ConId, Vec<Value>, Option<Vec<String>>),
    Closure {
        parameter: Id,
        body: Rc<Expr>,
        env: Rc<Env>,
        recursive: Option<Rc<Vec<Bind>>>,
    },
}
impl std::fmt::Display for Value {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.print(f, 0)
    }
}
impl Value {
    fn print(
        &self,
        f: &mut std::fmt::Formatter<'_>,
        precedence: u8,
    ) -> std::fmt::Result {
        match self {
            Self::Lit(Lit::Double(n))
                if f64::from_bits(*n).is_sign_negative()
                    && precedence > 6 =>
            {
                write!(
                    f,
                    "({})",
                    crate::primitives::show_double(f64::from_bits(*n))
                )
            }
            Self::Lit(Lit::Int(n)) if *n < 0 && precedence > 6 => {
                write!(f, "({n})")
            }
            Self::Lit(l) => write!(f, "{l}"),
            Self::Closure { .. } => write!(f, "<function>"),
            Self::Con(id, fields, Some(labels)) => {
                if precedence > 10 {
                    write!(f, "(")?;
                }
                write!(f, "{} {{", id.name)?;
                for (i, (name, value)) in
                    labels.iter().zip(fields).enumerate()
                {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{name} = ")?;
                    value.print(f, 0)?;
                }
                write!(f, "}}")?;
                if precedence > 10 {
                    write!(f, ")")?;
                }
                Ok(())
            }
            Self::Con(id, fields, None) => match id.name.as_str() {
                "[]" => write!(f, "[]"),
                ":" => {
                    write!(f, "[")?;
                    let mut value = self;
                    let mut first = true;
                    while let Self::Con(c, fs, _) = value {
                        if c.name == "[]" {
                            break;
                        }
                        if !first {
                            write!(f, ",")?;
                        }
                        first = false;
                        fs[0].print(f, 0)?;
                        value = &fs[1];
                    }
                    write!(f, "]")
                }
                "()" | "(,)" | "(,,)" => {
                    write!(f, "(")?;
                    for (i, v) in fields.iter().enumerate() {
                        if i > 0 {
                            write!(f, ",")?;
                        }
                        v.print(f, 0)?;
                    }
                    write!(f, ")")
                }
                _ => {
                    let parens = precedence > 10 && !fields.is_empty();
                    if parens {
                        write!(f, "(")?;
                    }
                    write!(f, "{}", id.name)?;
                    for v in fields {
                        write!(f, " ")?;
                        v.print(f, 11)?;
                    }
                    if parens {
                        write!(f, ")")?;
                    }
                    Ok(())
                }
            },
        }
    }
}
pub fn normalize(n: i64) -> i64 {
    (n << 1) >> 1
}
/// Rebuild a recursive group's closures from a shared immutable recipe. The
/// recipe captures the outer environment, never the closures themselves.
// ANCHOR: recursive-closures
fn recursive_values(bindings: Rc<Vec<Bind>>, env: Rc<Env>) -> Env {
    let mut values = Vec::new();
    for binding in bindings.iter() {
        let mut rhs = &*binding.rhs;
        while let Expr::TyLam(_, inner) = rhs {
            rhs = inner;
        }
        let Expr::Lam(parameter, body) = rhs else {
            unreachable!("recursive lambda invariant")
        };
        let value = Value::Closure {
            parameter: parameter.clone(),
            body: Rc::new(*body.clone()),
            env: env.clone(),
            recursive: Some(bindings.clone()),
        };
        values.push((binding.name.clone(), value));
    }
    values
}
fn bind_group(
    group: &BindGroup,
    env: &mut Env,
    fuel: &mut usize,
    program: &core::Program,
    globals: &RefCell<Env>,
) -> Result<(), Failure> {
    if group.recursive {
        env.extend(recursive_values(
            Rc::new(group.binds.clone()),
            Rc::new(env.clone()),
        ));
    } else {
        for binding in &group.binds {
            let value =
                evaluate(&binding.rhs, env, fuel, program, globals)?;
            env.push((binding.name.clone(), value));
        }
    }
    Ok(())
}
fn evaluate(
    expr: &Expr,
    env: &Env,
    fuel: &mut usize,
    program: &core::Program,
    globals: &RefCell<Env>,
) -> Result<Value, Failure> {
    *fuel = fuel.checked_sub(1).ok_or("step limit exceeded")?;
    match expr {
        Expr::Lit(l) => Ok(Value::Lit(l.clone())),
        Expr::TyLam(_, body) | Expr::TyApp(body, _) => {
            evaluate(body, env, fuel, program, globals)
        }
        Expr::Global(n) => globals
            .borrow()
            .iter()
            .find(|(id, _)| id.unique() == n.unique())
            .map(|(_, v)| v.clone())
            .ok_or_else(|| {
                Failure::Runtime(format!(
                    "internal error: uninitialized global {n}"
                ))
            }),
        Expr::Var(n) => env
            .iter()
            .rev()
            .find(|(name, _)| name == n)
            .map(|(_, v)| v.clone())
            .ok_or_else(|| {
                Failure::Runtime(format!("internal error: unbound {n}"))
            }),
        Expr::Lam(parameter, body) => Ok(Value::Closure {
            parameter: parameter.clone(),
            body: Rc::new(*body.clone()),
            env: Rc::new(env.clone()),
            recursive: None,
        }),
        // ANCHOR: application
        Expr::App(function, argument) => {
            let function = evaluate(function, env, fuel, program, globals)?;
            let argument = evaluate(argument, env, fuel, program, globals)?;
            if let Value::Closure {
                parameter,
                body,
                env,
                recursive,
            } = &function
            {
                let mut local = (**env).clone();
                if let Some(bindings) = recursive {
                    local.extend(recursive_values(
                        bindings.clone(),
                        env.clone(),
                    ));
                }
                local.push((parameter.clone(), argument));
                evaluate(body, &local, fuel, program, globals)
            } else {
                Err("internal error: non-function application".into())
            }
        }
        // ANCHOR_END: application
        Expr::Let(group, body) => {
            let mut local = env.clone();
            bind_group(group, &mut local, fuel, program, globals)?;
            evaluate(body, &local, fuel, program, globals)
        }
        Expr::Con(id, _, fields) => Ok(Value::Con(
            id.clone(),
            fields
                .iter()
                .map(|e| evaluate(e, env, fuel, program, globals))
                .collect::<Result<_, _>>()?,
            program.constructors[id.index as usize].labels.clone(),
        )),
        Expr::PatternFail(span, _) => Err(failure(program, *span).into()),
        Expr::Match(m) => {
            let values = m
                .scruts
                .iter()
                .map(|id| {
                    evaluate(
                        &Expr::Var(id.clone()),
                        env,
                        fuel,
                        program,
                        globals,
                    )
                })
                .collect::<Result<Vec<_>, _>>()?;
            for row in &m.rows {
                let mut local = env.clone();
                if !row
                    .pats
                    .iter()
                    .zip(&values)
                    .all(|(p, v)| matches_pattern(p, v, &mut local))
                {
                    continue;
                }
                for group in &row.locals {
                    bind_group(group, &mut local, fuel, program, globals)?;
                }
                match &row.body {
                    core::RowBody::Plain(e) => {
                        return evaluate(e, &local, fuel, program, globals);
                    }
                    core::RowBody::Guarded(gs) => {
                        for (g, e) in gs {
                            let value = evaluate(
                                g, &local, fuel, program, globals,
                            )?;
                            if matches!(value,Value::Con(id,_,_) if id.name=="True")
                            {
                                return evaluate(
                                    e, &local, fuel, program, globals,
                                );
                            }
                        }
                    }
                }
            }
            Err(failure(program, m.span).into())
        }
        Expr::Prim(op, _, args, _) => {
            let values = args
                .iter()
                .map(|a| evaluate(a, env, fuel, program, globals))
                .collect::<Result<Vec<_>, _>>()?;
            primitive(*op, &values, program, globals)
        }
    }
}
fn primitive(
    op: Op,
    args: &[Value],
    program: &core::Program,
    _globals: &RefCell<Env>,
) -> Result<Value, Failure> {
    if let Op::Runtime(p) = op {
        return crate::primitives::evaluate(p, args, program);
    }
    if let [Value::Lit(Lit::String(a)), Value::Lit(Lit::String(b))] = args
        && op == Op::Append
    {
        return Ok(Value::Lit(Lit::String(format!("{a}{b}"))));
    }
    let [Value::Lit(Lit::Int(a)), Value::Lit(Lit::Int(b))] = args else {
        return Err("internal error: primitive operands".into());
    };
    if matches!(op, Op::Eq | Op::Lt | Op::Le) {
        let yes = match op {
            Op::Eq => a == b,
            Op::Lt => a < b,
            _ => a <= b,
        };
        let name = if yes { "True" } else { "False" };
        let c = program
            .constructors
            .iter()
            .find(|c| c.id.name == name)
            .ok_or("internal error: missing Bool constructor")?;
        return Ok(Value::Con(c.id.clone(), vec![], c.labels.clone()));
    }
    let value = match op {
        Op::Runtime(p) => {
            return crate::primitives::evaluate(p, args, program);
        }
        Op::Add => Lit::Int(normalize(a.wrapping_add(*b))),
        Op::Sub => Lit::Int(normalize(a.wrapping_sub(*b))),
        Op::Mul => Lit::Int(normalize(a.wrapping_mul(*b))),
        Op::Eq | Op::Lt | Op::Le => unreachable!(),
        Op::Div | Op::Mod => {
            if *b == 0 {
                return Err("divide by zero".into());
            }
            let (mut q, mut r) = (a / b, a % b);
            if r != 0 && (r < 0) != (*b < 0) {
                q -= 1;
                r += b;
            }
            Lit::Int(normalize(if op == Op::Div { q } else { r }))
        }
        Op::Append => {
            return Err("internal error: string append on Int".into());
        }
    };
    Ok(Value::Lit(value))
}
/// Evaluate top-level values exactly once in dependency order, then select main's value.
fn execute(
    program: &core::Program,
    mut fuel: usize,
    io: bool,
) -> Result<String, Failure> {
    let globals = RefCell::new(vec![]);
    for group in &program.groups {
        for b in &group.binds {
            if core::is_function(&b.rhs) {
                let value = evaluate(
                    &b.rhs,
                    &vec![],
                    &mut fuel,
                    program,
                    &globals,
                )?;
                globals.borrow_mut().push((b.name.clone(), value));
            }
        }
    }
    for group in &program.groups {
        for b in &group.binds {
            if !core::is_function(&b.rhs) {
                let value = evaluate(
                    &b.rhs,
                    &vec![],
                    &mut fuel,
                    program,
                    &globals,
                )?;
                globals.borrow_mut().push((b.name.clone(), value));
            }
        }
    }
    let value = globals
        .borrow()
        .iter()
        .find(|(n, _)| n.unique() == program.main.unique())
        .map(|(_, v)| v.clone())
        .ok_or("internal error: missing main")?;
    if io {
        let unit = program
            .constructors
            .iter()
            .find(|c| c.id.name == "()")
            .unwrap();
        let action = Expr::App(
            Box::new(Expr::Global(program.main.clone())),
            Box::new(Expr::Con(unit.id.clone(), vec![], vec![])),
        );
        evaluate(&action, &vec![], &mut fuel, program, &globals)?;
        Ok(String::new())
    } else {
        Ok(value.to_string())
    }
}
/// Evaluate a pure fragment; retained for compiler tests and REPL inspection.
pub fn run(program: &core::Program, fuel: usize) -> Result<String, String> {
    execute(program, fuel, false).map_err(|e| e.to_string())
}
fn failure(program: &core::Program, span: crate::source::Span) -> String {
    let site = program
        .locations
        .iter()
        .find(|(s, _)| *s == span)
        .map(|(_, s)| s.as_str())
        .unwrap_or("<unknown>:1:1");
    format!("pattern match failure at {site}")
}
fn matches_pattern(p: &core::Pat, v: &Value, env: &mut Env) -> bool {
    match p {
        core::Pat::Wild => true,
        core::Pat::Var(id) => {
            env.push((id.clone(), v.clone()));
            true
        }
        core::Pat::As(id, p) => {
            env.push((id.clone(), v.clone()));
            matches_pattern(p, v, env)
        }
        core::Pat::Lit(Lit::Double(n)) => {
            matches!(v,Value::Lit(Lit::Double(m)) if f64::from_bits(*n)==f64::from_bits(*m))
        }
        core::Pat::Lit(l) => matches!(v,Value::Lit(actual) if l==actual),
        core::Pat::Con(id, ps) => match v {
            Value::Con(actual, vs, _)
                if id == actual && ps.len() == vs.len() =>
            {
                ps.iter().zip(vs).all(|(p, v)| matches_pattern(p, v, env))
            }
            _ => false,
        },
    }
}
/// Initialize globals, then run the checked IO action exactly once.
pub fn run_io(program: &core::Program, fuel: usize) -> Result<(), Failure> {
    execute(program, fuel, true).map(|_| ())
}
}

사용되지 않는 최상위 액션도 여전히 시작 시점에 초기화되는 값입니다. 다음 회귀 테스트가 보여 주듯이, 그 본문에는 진입하지 않습니다:

unused = putStrLn "bad"

main = putStrLn "ok"

평가기는 언어 오류와 요청된 프로세스 종료를 구별합니다. 이 구별 덕분에 exitWith 7이 오류 메시지로 출력되지 않습니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::Op,
    core::{self, Bind, BindGroup, Expr, Id, Lit},
};
use std::{cell::RefCell, rc::Rc};
impl From<String> for Failure {
    fn from(s: String) -> Self {
        Self::Runtime(s)
    }
}
impl From<&str> for Failure {
    fn from(s: &str) -> Self {
        Self::Runtime(s.into())
    }
}
impl std::fmt::Display for Failure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Runtime(s) => write!(f, "{s}"),
            Self::Exit(n) => write!(f, "exit {n}"),
        }
    }
}
type Env = Vec<(Id, Value)>;
#[derive(Clone, Debug)]
/// A closure captures outer declarations and an optional immutable recursive-group recipe.
pub enum Value {
    Lit(Lit),
    Con(crate::data::ConId, Vec<Value>, Option<Vec<String>>),
    Closure {
        parameter: Id,
        body: Rc<Expr>,
        env: Rc<Env>,
        recursive: Option<Rc<Vec<Bind>>>,
    },
}
impl std::fmt::Display for Value {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.print(f, 0)
    }
}
impl Value {
    fn print(
        &self,
        f: &mut std::fmt::Formatter<'_>,
        precedence: u8,
    ) -> std::fmt::Result {
        match self {
            Self::Lit(Lit::Double(n))
                if f64::from_bits(*n).is_sign_negative()
                    && precedence > 6 =>
            {
                write!(
                    f,
                    "({})",
                    crate::primitives::show_double(f64::from_bits(*n))
                )
            }
            Self::Lit(Lit::Int(n)) if *n < 0 && precedence > 6 => {
                write!(f, "({n})")
            }
            Self::Lit(l) => write!(f, "{l}"),
            Self::Closure { .. } => write!(f, "<function>"),
            Self::Con(id, fields, Some(labels)) => {
                if precedence > 10 {
                    write!(f, "(")?;
                }
                write!(f, "{} {{", id.name)?;
                for (i, (name, value)) in
                    labels.iter().zip(fields).enumerate()
                {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{name} = ")?;
                    value.print(f, 0)?;
                }
                write!(f, "}}")?;
                if precedence > 10 {
                    write!(f, ")")?;
                }
                Ok(())
            }
            Self::Con(id, fields, None) => match id.name.as_str() {
                "[]" => write!(f, "[]"),
                ":" => {
                    write!(f, "[")?;
                    let mut value = self;
                    let mut first = true;
                    while let Self::Con(c, fs, _) = value {
                        if c.name == "[]" {
                            break;
                        }
                        if !first {
                            write!(f, ",")?;
                        }
                        first = false;
                        fs[0].print(f, 0)?;
                        value = &fs[1];
                    }
                    write!(f, "]")
                }
                "()" | "(,)" | "(,,)" => {
                    write!(f, "(")?;
                    for (i, v) in fields.iter().enumerate() {
                        if i > 0 {
                            write!(f, ",")?;
                        }
                        v.print(f, 0)?;
                    }
                    write!(f, ")")
                }
                _ => {
                    let parens = precedence > 10 && !fields.is_empty();
                    if parens {
                        write!(f, "(")?;
                    }
                    write!(f, "{}", id.name)?;
                    for v in fields {
                        write!(f, " ")?;
                        v.print(f, 11)?;
                    }
                    if parens {
                        write!(f, ")")?;
                    }
                    Ok(())
                }
            },
        }
    }
}
pub fn normalize(n: i64) -> i64 {
    (n << 1) >> 1
}
/// Rebuild a recursive group's closures from a shared immutable recipe. The
/// recipe captures the outer environment, never the closures themselves.
// ANCHOR: recursive-closures
fn recursive_values(bindings: Rc<Vec<Bind>>, env: Rc<Env>) -> Env {
    let mut values = Vec::new();
    for binding in bindings.iter() {
        let mut rhs = &*binding.rhs;
        while let Expr::TyLam(_, inner) = rhs {
            rhs = inner;
        }
        let Expr::Lam(parameter, body) = rhs else {
            unreachable!("recursive lambda invariant")
        };
        let value = Value::Closure {
            parameter: parameter.clone(),
            body: Rc::new(*body.clone()),
            env: env.clone(),
            recursive: Some(bindings.clone()),
        };
        values.push((binding.name.clone(), value));
    }
    values
}
fn bind_group(
    group: &BindGroup,
    env: &mut Env,
    fuel: &mut usize,
    program: &core::Program,
    globals: &RefCell<Env>,
) -> Result<(), Failure> {
    if group.recursive {
        env.extend(recursive_values(
            Rc::new(group.binds.clone()),
            Rc::new(env.clone()),
        ));
    } else {
        for binding in &group.binds {
            let value =
                evaluate(&binding.rhs, env, fuel, program, globals)?;
            env.push((binding.name.clone(), value));
        }
    }
    Ok(())
}
fn evaluate(
    expr: &Expr,
    env: &Env,
    fuel: &mut usize,
    program: &core::Program,
    globals: &RefCell<Env>,
) -> Result<Value, Failure> {
    *fuel = fuel.checked_sub(1).ok_or("step limit exceeded")?;
    match expr {
        Expr::Lit(l) => Ok(Value::Lit(l.clone())),
        Expr::TyLam(_, body) | Expr::TyApp(body, _) => {
            evaluate(body, env, fuel, program, globals)
        }
        Expr::Global(n) => globals
            .borrow()
            .iter()
            .find(|(id, _)| id.unique() == n.unique())
            .map(|(_, v)| v.clone())
            .ok_or_else(|| {
                Failure::Runtime(format!(
                    "internal error: uninitialized global {n}"
                ))
            }),
        Expr::Var(n) => env
            .iter()
            .rev()
            .find(|(name, _)| name == n)
            .map(|(_, v)| v.clone())
            .ok_or_else(|| {
                Failure::Runtime(format!("internal error: unbound {n}"))
            }),
        Expr::Lam(parameter, body) => Ok(Value::Closure {
            parameter: parameter.clone(),
            body: Rc::new(*body.clone()),
            env: Rc::new(env.clone()),
            recursive: None,
        }),
        // ANCHOR: application
        Expr::App(function, argument) => {
            let function = evaluate(function, env, fuel, program, globals)?;
            let argument = evaluate(argument, env, fuel, program, globals)?;
            if let Value::Closure {
                parameter,
                body,
                env,
                recursive,
            } = &function
            {
                let mut local = (**env).clone();
                if let Some(bindings) = recursive {
                    local.extend(recursive_values(
                        bindings.clone(),
                        env.clone(),
                    ));
                }
                local.push((parameter.clone(), argument));
                evaluate(body, &local, fuel, program, globals)
            } else {
                Err("internal error: non-function application".into())
            }
        }
        // ANCHOR_END: application
        Expr::Let(group, body) => {
            let mut local = env.clone();
            bind_group(group, &mut local, fuel, program, globals)?;
            evaluate(body, &local, fuel, program, globals)
        }
        Expr::Con(id, _, fields) => Ok(Value::Con(
            id.clone(),
            fields
                .iter()
                .map(|e| evaluate(e, env, fuel, program, globals))
                .collect::<Result<_, _>>()?,
            program.constructors[id.index as usize].labels.clone(),
        )),
        Expr::PatternFail(span, _) => Err(failure(program, *span).into()),
        Expr::Match(m) => {
            let values = m
                .scruts
                .iter()
                .map(|id| {
                    evaluate(
                        &Expr::Var(id.clone()),
                        env,
                        fuel,
                        program,
                        globals,
                    )
                })
                .collect::<Result<Vec<_>, _>>()?;
            for row in &m.rows {
                let mut local = env.clone();
                if !row
                    .pats
                    .iter()
                    .zip(&values)
                    .all(|(p, v)| matches_pattern(p, v, &mut local))
                {
                    continue;
                }
                for group in &row.locals {
                    bind_group(group, &mut local, fuel, program, globals)?;
                }
                match &row.body {
                    core::RowBody::Plain(e) => {
                        return evaluate(e, &local, fuel, program, globals);
                    }
                    core::RowBody::Guarded(gs) => {
                        for (g, e) in gs {
                            let value = evaluate(
                                g, &local, fuel, program, globals,
                            )?;
                            if matches!(value,Value::Con(id,_,_) if id.name=="True")
                            {
                                return evaluate(
                                    e, &local, fuel, program, globals,
                                );
                            }
                        }
                    }
                }
            }
            Err(failure(program, m.span).into())
        }
        Expr::Prim(op, _, args, _) => {
            let values = args
                .iter()
                .map(|a| evaluate(a, env, fuel, program, globals))
                .collect::<Result<Vec<_>, _>>()?;
            primitive(*op, &values, program, globals)
        }
    }
}
fn primitive(
    op: Op,
    args: &[Value],
    program: &core::Program,
    _globals: &RefCell<Env>,
) -> Result<Value, Failure> {
    if let Op::Runtime(p) = op {
        return crate::primitives::evaluate(p, args, program);
    }
    if let [Value::Lit(Lit::String(a)), Value::Lit(Lit::String(b))] = args
        && op == Op::Append
    {
        return Ok(Value::Lit(Lit::String(format!("{a}{b}"))));
    }
    let [Value::Lit(Lit::Int(a)), Value::Lit(Lit::Int(b))] = args else {
        return Err("internal error: primitive operands".into());
    };
    if matches!(op, Op::Eq | Op::Lt | Op::Le) {
        let yes = match op {
            Op::Eq => a == b,
            Op::Lt => a < b,
            _ => a <= b,
        };
        let name = if yes { "True" } else { "False" };
        let c = program
            .constructors
            .iter()
            .find(|c| c.id.name == name)
            .ok_or("internal error: missing Bool constructor")?;
        return Ok(Value::Con(c.id.clone(), vec![], c.labels.clone()));
    }
    let value = match op {
        Op::Runtime(p) => {
            return crate::primitives::evaluate(p, args, program);
        }
        Op::Add => Lit::Int(normalize(a.wrapping_add(*b))),
        Op::Sub => Lit::Int(normalize(a.wrapping_sub(*b))),
        Op::Mul => Lit::Int(normalize(a.wrapping_mul(*b))),
        Op::Eq | Op::Lt | Op::Le => unreachable!(),
        Op::Div | Op::Mod => {
            if *b == 0 {
                return Err("divide by zero".into());
            }
            let (mut q, mut r) = (a / b, a % b);
            if r != 0 && (r < 0) != (*b < 0) {
                q -= 1;
                r += b;
            }
            Lit::Int(normalize(if op == Op::Div { q } else { r }))
        }
        Op::Append => {
            return Err("internal error: string append on Int".into());
        }
    };
    Ok(Value::Lit(value))
}
/// Evaluate top-level values exactly once in dependency order, then select main's value.
fn execute(
    program: &core::Program,
    mut fuel: usize,
    io: bool,
) -> Result<String, Failure> {
    let globals = RefCell::new(vec![]);
    for group in &program.groups {
        for b in &group.binds {
            if core::is_function(&b.rhs) {
                let value = evaluate(
                    &b.rhs,
                    &vec![],
                    &mut fuel,
                    program,
                    &globals,
                )?;
                globals.borrow_mut().push((b.name.clone(), value));
            }
        }
    }
    for group in &program.groups {
        for b in &group.binds {
            if !core::is_function(&b.rhs) {
                let value = evaluate(
                    &b.rhs,
                    &vec![],
                    &mut fuel,
                    program,
                    &globals,
                )?;
                globals.borrow_mut().push((b.name.clone(), value));
            }
        }
    }
    let value = globals
        .borrow()
        .iter()
        .find(|(n, _)| n.unique() == program.main.unique())
        .map(|(_, v)| v.clone())
        .ok_or("internal error: missing main")?;
    if io {
        let unit = program
            .constructors
            .iter()
            .find(|c| c.id.name == "()")
            .unwrap();
        let action = Expr::App(
            Box::new(Expr::Global(program.main.clone())),
            Box::new(Expr::Con(unit.id.clone(), vec![], vec![])),
        );
        evaluate(&action, &vec![], &mut fuel, program, &globals)?;
        Ok(String::new())
    } else {
        Ok(value.to_string())
    }
}
/// Evaluate a pure fragment; retained for compiler tests and REPL inspection.
pub fn run(program: &core::Program, fuel: usize) -> Result<String, String> {
    execute(program, fuel, false).map_err(|e| e.to_string())
}
/// Initialize globals, then run the checked IO action exactly once.
pub fn run_io(program: &core::Program, fuel: usize) -> Result<(), Failure> {
    execute(program, fuel, true).map(|_| ())
}
fn failure(program: &core::Program, span: crate::source::Span) -> String {
    let site = program
        .locations
        .iter()
        .find(|(s, _)| *s == span)
        .map(|(_, s)| s.as_str())
        .unwrap_or("<unknown>:1:1");
    format!("pattern match failure at {site}")
}
fn matches_pattern(p: &core::Pat, v: &Value, env: &mut Env) -> bool {
    match p {
        core::Pat::Wild => true,
        core::Pat::Var(id) => {
            env.push((id.clone(), v.clone()));
            true
        }
        core::Pat::As(id, p) => {
            env.push((id.clone(), v.clone()));
            matches_pattern(p, v, env)
        }
        core::Pat::Lit(Lit::Double(n)) => {
            matches!(v,Value::Lit(Lit::Double(m)) if f64::from_bits(*n)==f64::from_bits(*m))
        }
        core::Pat::Lit(l) => matches!(v,Value::Lit(actual) if l==actual),
        core::Pat::Con(id, ps) => match v {
            Value::Con(actual, vs, _)
                if id == actual && ps.len() == vs.len() =>
            {
                ps.iter().zip(vs).all(|(p, v)| matches_pattern(p, v, env))
            }
            _ => false,
        },
    }
}
/// Language termination is separate from an ordinary runtime failure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Failure {
    Runtime(String),
    Exit(i32),
}
}

드라이버는 실행이 끝날 때와 런타임 진단을 출력하기 전에 stdout을 플러시합니다. 입력 프리미티브는 읽기 전에 플러시하므로, 프로그램이 블록되기 전에 프롬프트가 사용자에게 전달됩니다. 테스트 실행기는 .in 파일을 받아 각 엔진에 동일한 입력을 보냅니다. 파이프 핸드셰이크 회귀 테스트는 터미널 버퍼링과 무관하게 플러시를 검사합니다.

여기서 기존 프로그램은 약간 손봐야 합니다. 드라이버가 더 이상 main을 대신 출력해 주지 않으므로, 출력 코드를 프로그램 안에 직접 넣습니다. 값에 Show 인스턴스가 없다면 인스턴스를 직접 제공하거나 그 값에서 출력할 수 있는 부분을 골라야 합니다. 기존 값 프린터는 컴파일러 조각을 검사할 수 있도록 --print-value를 통해 계속 사용할 수 있습니다.

실행해 보기

저장소 루트에서 이 장의 크레이트로 들어가십시오. 다음 명령은 명시적인 액션의 출력과 추론된 진입점 타입을 출력합니다:

$ cd crates/ch15_io
$ cargo run -q -- run tests/programs/io/hello.fun --lint
hello
$ cargo run -q -- dump --pass=types tests/programs/io/hello.fun
main : IO ()
$ cargo run -q -- run tests/programs/io/deferred.fun --lint
ok

--pass=core를 사용하면 사전 인자와 unit을 받는 람다를 찾을 수 있습니다. 사용되지 않는 출력 액션과 main이 실제로 순차 실행하는 출력 액션을 비교해 볼 만합니다. 둘 다 값이지만, 본문이 실행되는 것은 후자뿐입니다. 이 장에서도 우리의 실행 엔진은 여전히 Core 인터프리터입니다.

연습 문제

  1. 준비 운동. 예제의 두 번째 Just를 적절히 타입을 표기한 Nothing으로 바꾸십시오. 그 컨티뉴에이션이 호출되지 않는 이유를 설명하십시오.
  2. 준비 운동. 사용되지 않는 출력 액션을 만드는 경우와, 문자열 인자가 error를 호출하는 출력 액션을 만드는 경우를 비교하십시오.
  3. 중급. let, 튜플 바인드, 섀도잉 바인드를 포함하는 do 블록에 대해 생성되는 스코프를 추적하십시오.
  4. 중급. Core에서 Monad IO 사전을 살펴보고 IO 낮추기(lowering)의 영향을 받는 모든 타입을 찾아내십시오.
  5. 고급. 파이프 핸드셰이크 테스트를 확장하여 프롬프트를 두 번 내보내고, 터미널의 줄 버퍼링에 의존하지 않고 그 순서를 검증하십시오.

더 읽을거리

16장: 패턴 매칭 컴파일

새 파일

  • src/match_compile.rs
  • src/usefulness.rs
  • src/env.rs

변경된 파일

  • src/core.rs
  • src/core_lint.rs
  • src/eval.rs
  • src/io_lower.rs
  • src/lib.rs
  • src/main.rs

동기

우리 인터프리터는 패턴을 위에서 아래로 차례대로 시도하므로 그 의미를 따라가기 쉽습니다. 네이티브 컴파일러는 이 과정을 태그를 검사하고, 필드를 읽어 오고, 분기하는 명령어로 바꾸어야 합니다. 각 패턴을 따로따로 번역할 수도 있지만, 그러면 같은 값을 몇 번이고 다시 검사하는 일이 잦을 것입니다.

대신 우리는 패턴들을 한꺼번에 살펴보겠습니다. 패턴들을 행렬에 놓고, 쓸모 있는 검사를 하나 고른 다음, 그 결과에 따라 남은 작업을 나누십시오. 이렇게 하면 결정 트리가 만들어집니다. 합류 지점(join point)을 쓰면 서로 다른 경로가 도달하는 코드를 공유할 수 있으며, 가드가 실패했을 때 넘어가는 다음 절도 여기에 포함됩니다.

또한 어떤 입력이 패턴들 사이로 빠져나가지는 않는지, 혹은 뒤쪽 절이 매칭할 수 있는 것을 앞쪽 절이 이미 모두 잡아 버리지는 않았는지도 따져 볼 수 있습니다. 이는 흐뭇한 연결 고리입니다. 값의 모양을 이해하면 패턴을 실행하는 데에도, 패턴 속 실수를 설명하는 데에도 도움이 됩니다.

현재의 언어

새로운 소스 구문은 없습니다. 이전 프로그램들은 패턴이 매칭된 뒤에 실패하는 가드를 포함하여 모두 같은 결과를 냅니다:

f (Just x) | x < 0 = 0
f (Just x) = x
f Nothing = 99

main = print (f (Just 42))

첫 번째 절은 Just 42를 알아보지만, 그 가드가 실패합니다. 실행은 두 번째 절을 시도하여 42를 반환해야 합니다. 패턴 매칭 실패를 보고하거나 Nothing 절로 건너뛰어서는 안 됩니다. 지역 where 바인딩은 자신이 속한 등식의 가드와 우변에서 계속 스코프 안에 있습니다.

이제 검사기는 match에 난 구멍을 보여 줄 수 있습니다. W0001에는 처리되지 않는 입력의 예가 포함되며, W0002는 앞선 절 때문에 도달할 수 없게 된 절을 짚어 줍니다. 이들은 경고이므로 프로그램은 여전히 실행할 수 있습니다. 런타임에 처리되지 않은 입력을 만나면 여전히 fun: pattern match failure at FILE:LINE:COL을 보고합니다.

이론

match를 행이 절이고 열이 검사 대상(scrutinee)인 행렬로 생각해 보십시오. 생성자 C에 대해 열을 특수화하면 C와 매치될 수 있는 행을 남기고, 그 생성자를 제거한 뒤, 그 필드들을 새 열로 삽입합니다. 기본 행렬(default matrix)은 선택된 패턴이 다른 어떤 값과도 매치될 수 있는 행을 유지합니다. 이것이 Maranget이 설명한 결정 트리 방식입니다 (Maranget 2008).

Fun은 첫 번째 행에서 반박 가능한 패턴을 담고 있는 첫 번째 열을 고릅니다. 이 단순한 휴리스틱은 결정적이며 추적하기 쉽습니다. 이것이 모든 행렬에 대해 검사 횟수를 최소화한다는 주장은 아닙니다. 와일드카드, 변수, as-패턴은 값을 검사하지 않습니다. 이들의 바인딩은 최종적으로 실행될 절 본문을 위해 기록됩니다. 생성자 패턴은 태그가 매치된 뒤에야 필드를 드러냅니다.

유한한 데이터 타입은 알려진 생성자 집합을 가집니다. 정수와 문자 리터럴에는 기본 경로가 필요합니다. 검사한 리터럴의 유한한 집합으로는 그 값의 정의역을 모두 덮을 수 없기 때문입니다. 문자열 패턴은 동등성 검사가 됩니다. Double 패턴도 기본 동등성을 사용하며, 이를 통해 양의 0과 음의 0이 같다는 점을 포함한 언어의 부동소수점 비교 동작을 그대로 유지합니다.

유용성(usefulness)은 후보 패턴 벡터가 앞선 커버링 행들이 매치하지 않는 값을 허용하는지를 묻습니다 (Maranget 2007). 유용하지 않은 소스 행은 중복입니다. 유용한 전체 와일드카드 후보는 누락된 경우의 증거(witness)를 제공합니다. 가드는 커버리지 판단을 복잡하게 만듭니다. 패턴이 매치되더라도 다음 절로 넘어갈 수 있기 때문입니다. 구현은 가드 중 하나가 문자 그대로 True일 때만 가드가 있는 행을 커버링 행으로 취급합니다. 임의의 불리언 식을 증명하려고 시도하지는 않습니다.

하나의 질문, 두 가지 검사

두 경고를 같은 질문으로 바꾸는 깔끔한 방법이 있습니다. 이 패턴은 앞선 패턴들이 놓친 무언가와 매치됩니까? Maranget은 이런 패턴을 유용하다고 부릅니다. 앞선 행들을 P, 후보 패턴 벡터를 q라 할 때, 이 질문을 U(P, q)로 쓰겠습니다. 증거(witness)란 P를 빠져나가면서 q와 매치되는 실제 형태입니다 (Maranget 2007).

도달 가능성을 검사할 때 P는 현재 절보다 앞선 덮는 행들을 담고, q는 그 절의 패턴 벡터입니다. 완전성을 따질 때는 P가 커버하는 모든 행을 담고, q는 전부 와일드카드로 이루어진 벡터입니다. 첫 번째 질의는 어떤 절이 새로운 경우를 하나라도 추가하는지 묻습니다. 두 번째 질의는 아직 빠진 것이 남아 있는지 묻습니다. 커버리지를 결정하는 것은 행들 전체입니다. 앞선 절 가운데 어느 하나도 어떤 절을 완전히 덮지 못하더라도 그 절은 도달 불가능할 수 있습니다.

Maybe Bool과 Bool, 두 인자를 받는 함수를 생각해 봅시다:

choose Nothing     _     = 0
choose (Just True) _     = 1
choose (Just _)    False = 2

우변은 무시하고 인자 열만 남겨 둡시다:

행첫 번째 인자두 번째 인자
1Nothing_
2Just True_
3Just _False

(_, _)가 유용한지 물어봅시다. 첫 번째 열에 Maybe의 두 생성자가 모두 들어 있으므로, 생성자 집합의 각 구성원을 하나씩 살펴봅니다. Nothing으로 특수화하면 2행과 3행이 버려지고 두 번째 인자 자리에 와일드카드가 남습니다. 그 분기는 덮여 있습니다. Just로 특수화하면 1행이 버려지고 첫 번째 열이 그 생성자의 유일한 필드로 바뀝니다:

행Just 안의 값두 번째 인자
2True_
3_False

True 분기는 2행이 덮습니다. False 분기에는 3행만 남으며, 이 행은 두 번째 인자가 False인 경우를 덮습니다. 덮이지 않은 두 번째 인자는 True입니다. 필드를 감싸 생성자를 다시 세우면 원래 인자 벡터에 대한 증인 (Just False, True)를 얻습니다. choose (Just False) True = 3을 추가하면 이 매치가 완전해집니다. 원래의 어떤 절도 혼자서는 모든 입력을 덮지 못했지만, 그 뒤에 오는 절은 모두 도달 불가능합니다.

특수화와 기본 행렬

usefulness.rs는 첫 번째 열에 대해 탐색을 구현합니다. 선택된 패턴과 생성자 집합이 다음 문제를 결정합니다:

  1. 생성자 후보. C(p1, ..., pk)의 경우, C나 와일드카드로 시작하는 행을 남깁니다. C를 그 필드들로 바꿉니다. 와일드카드는 k개의 와일드카드로 확장합니다. 다른 생성자로 시작하는 행은 버리고, 질의에도 같은 확장을 적용합니다.
  2. 와일드카드 후보, 완전한 족. 열의 데이터 타입에 속한 생성자가 모두 나타나면 각 생성자에 대해 특수화합니다. 유용한 분기가 하나라도 있으면 그 분기가 증인을 제공합니다.
  3. 와일드카드 후보, 불완전한 족. 기본 행렬을 탐색합니다. 와일드카드로 시작하는 행만 남기고 그 첫 번째 열을 버립니다. 유용한 접미사 앞에는 빠진 생성자를 붙일 수 있습니다. 리터럴 도메인은 이 경로로 처리하며, 검사한 머리에 없는 리터럴을 하나 고릅니다.

열이 0개인 경우에는 빈 행렬과 빈 행을 구별합니다. 행이 없으면 남은 빈 벡터를 덮는 것이 없으므로 그 벡터는 유용합니다. 빈 행이 있으면 앞선 절이 이미 남은 모든 것과 매치되므로 유용하지 않습니다. 이 작은 기저 사례들 덕분에 탐색은 재귀 데이터 타입의 모든 값을 열거하지 않고도 중첩된 생성자를 처리할 수 있습니다.

알고리즘은 돌아오면서 벗겨 냈던 생성자를 각각 증인 필드를 감싸 다시 세웁니다. 그래서 Fun의 useful은 단순한 불리언이 아니라 선택적인 패턴 벡터를 반환합니다. 경고 드라이버는 빠진 구체적인 형태를 출력하고, 그 형태를 임시 커버리지 행렬에 추가한 뒤, 최대 세 개의 예시를 찾을 때까지 다시 탐색할 수 있습니다.

가드와 커버리지의 한계

가드가 있는 절은 앞선 절들이 이미 그 패턴을 덮고 있어서 도달 불가능할 수 있습니다. 그러나 가드가 실패할 수 있으므로, 대개 그 절 자체로는 커버리지를 확립하지 못합니다. Fun은 그 행을 앞선 커버리지와 대조해 검사한 뒤, 가드가 없거나 구문상 리터럴 True 가드를 가진 경우에만 커버리지에 추가합니다. 임의의 가드 표현식을 풀지는 않습니다. 따라서 보고된 빈틈은 이 보수적인 정책을 반영한 것일 수 있으며, 패턴이 유용하다고 해서 그 가드가 언젠가 성공한다는 것이 증명되지는 않습니다.

이 분석은 타입이 붙은 생성자 집합을 사용하며, 변수를 와일드카드로 취급합니다. as-패턴의 이름은 커버리지를 바꾸지 않습니다. 재귀적인 필드 타입에 값이 존재하는지(inhabited)는 증명하지 않습니다. 컴파일과 검사는 별개로 유지됩니다. 결정 트리는 이러한 경고와 관계없이 절의 순서와 가드 폴스루(fallthrough)를 보존해야 합니다.

결정 트리 공유하기

조인 포인트는 어떤 식에 국한된 컨티뉴에이션에 이름을 붙인 것입니다. 조인 포인트의 매개변수는 공유되는 본문에 필요한 값을 전달하며, 모든 점프는 완전히 적용된 꼬리 전달입니다. 일반 함수와 달리, 조인은 탈출하거나 값으로 전달될 수 없습니다. 이 구분은 이후 ANF에서 조인을 사용하는 방식을 미리 보여 줍니다 (Maurer et al. 2017).

구현

컴파일러의 내부 행은 남은 패턴과 최종 우변을 분리합니다. values는 현재 결정 경로를 따라 바인딩된 값을 기록합니다. target과 params는 공유되는 절 본문을 식별합니다.

#![allow(unused)]
fn main() {
use crate::{
    core::{self, Alt, AltCon, Expr, Id, Pat, RowBody, Type},
    names,
    session::{Session, Stop},
    source::Span,
};
use std::collections::BTreeMap;
struct Compiler<'a> {
    sess: &'a mut Session,
    constructors: Vec<core::ConDecl>,
    truths: std::collections::BTreeSet<u32>,
}
impl Compiler<'_> {
    fn fresh(&mut self, ty: Type) -> Id {
        Id {
            name: self.sess.fresh_id("$case"),
            ty,
        }
    }
    fn fields(&self, con: &crate::data::ConId, ty: &Type) -> Vec<Type> {
        let c = &self.constructors[con.index as usize];
        let args = match ty {
            Type::Con(_, args) => args.as_slice(),
            Type::Bool => &[],
            _ => unreachable!("typed data scrutinee"),
        };
        c.fields
            .iter()
            .map(|t| crate::elaborate::instantiate(t, &c.parameters, args))
            .collect()
    }
    fn boolean(
        &mut self,
        test: Expr,
        yes: Expr,
        no: Expr,
        result: &Type,
    ) -> Expr {
        let truth = self
            .constructors
            .iter()
            .find(|c| c.id.name == "True")
            .unwrap()
            .id
            .clone();
        Expr::Case(
            Box::new(test),
            self.fresh(Type::Bool),
            result.clone(),
            vec![
                Alt {
                    con: AltCon::Con(truth),
                    binders: vec![],
                    rhs: yes,
                },
                Alt {
                    con: AltCon::Default,
                    binders: vec![],
                    rhs: no,
                },
            ],
        )
    }
    fn group(&mut self, g: core::BindGroup) -> core::BindGroup {
        core::BindGroup {
            recursive: g.recursive,
            binds: g
                .binds
                .into_iter()
                .map(|mut b| {
                    b.rhs = Box::new(self.expr(*b.rhs));
                    b
                })
                .collect(),
        }
    }
    fn expr(&mut self, e: Expr) -> Expr {
        match e {
            Expr::Match(m) => self.compile(m),
            Expr::Lam(i, b) => Expr::Lam(i, Box::new(self.expr(*b))),
            Expr::TyLam(v, b) => Expr::TyLam(v, Box::new(self.expr(*b))),
            Expr::TyApp(b, t) => Expr::TyApp(Box::new(self.expr(*b)), t),
            e @ Expr::App(..) => {
                let mut work = vec![Some(e)];
                let mut output = Vec::new();
                while let Some(job) = work.pop() {
                    match job {
                        Some(Expr::App(a, b)) => {
                            work.push(None);
                            work.push(Some(*b));
                            work.push(Some(*a));
                        }
                        Some(e) => output.push(self.expr(e)),
                        None => {
                            let b = output.pop().unwrap();
                            let a = output.pop().unwrap();
                            output
                                .push(Expr::App(Box::new(a), Box::new(b)));
                        }
                    }
                }
                output.pop().unwrap()
            }
            Expr::Let(g, b) => {
                Expr::Let(self.group(g), Box::new(self.expr(*b)))
            }
            Expr::Con(c, ts, args) => Expr::Con(
                c,
                ts,
                args.into_iter().map(|e| self.expr(e)).collect(),
            ),
            Expr::Prim(p, ts, args, s) => Expr::Prim(
                p,
                ts,
                args.into_iter().map(|e| self.expr(e)).collect(),
                s,
            ),
            other => other,
        }
    }
    fn compile(&mut self, m: core::Match) -> Expr {
        crate::usefulness::warn(
            self.sess,
            &m,
            &self.constructors,
            &self.truths,
        );
        let mut rows = vec![];
        let mut bodies = vec![];
        for r in m.rows {
            let mut params = vec![];
            for p in &r.pats {
                binders(p, &mut params)
            }
            let target = self.sess.fresh_id("$rhs");
            rows.push(Row {
                pats: r.pats,
                values: BTreeMap::new(),
                target: target.clone(),
                params: params.clone(),
            });
            bodies.push((target, params, r.locals, r.body));
        }
        let mut definitions = vec![];
        for (index, (target, params, locals, body)) in
            bodies.into_iter().enumerate().rev()
        {
            let fail = self.sess.fresh_id("$fail");
            let mut body = match body {
                RowBody::Plain(e) => self.expr(*e),
                RowBody::Guarded(gs) => {
                    let residual = self.tree(
                        m.scruts.clone(),
                        rows[index + 1..].to_vec(),
                        &m.result,
                        m.span,
                    );
                    definitions.push((fail.clone(), vec![], residual));
                    let mut fallback =
                        Expr::Jump(fail.clone(), vec![], m.result.clone());
                    for (g, e) in gs.into_iter().rev() {
                        let g = self.expr(g);
                        let e = self.expr(e);
                        fallback = self.boolean(g, e, fallback, &m.result)
                    }
                    fallback
                }
            };
            for g in locals.into_iter().rev() {
                body = Expr::Let(self.group(g), Box::new(body))
            }
            definitions.push((target, params, body));
        }
        let mut body = self.tree(m.scruts, rows, &m.result, m.span);
        for (id, params, rhs) in definitions.into_iter().rev() {
            body = Expr::Join(id, params, Box::new(rhs), Box::new(body))
        }
        body
    }
    fn tree(
        &mut self,
        scruts: Vec<Id>,
        mut rows: Vec<Row>,
        result: &Type,
        span: Span,
    ) -> Expr {
        if rows.is_empty() {
            return Expr::PatternFail(span, result.clone());
        }
        // Bind variables/as-patterns without copying their unique Core binders.
        for row in &mut rows {
            for (p, s) in row.pats.iter_mut().zip(&scruts) {
                loop {
                    match p.clone() {
                        Pat::Var(id) => {
                            row.values.insert(id.unique(), s.clone());
                            *p = Pat::Wild;
                            break;
                        }
                        Pat::As(id, inner) => {
                            row.values.insert(id.unique(), s.clone());
                            *p = *inner
                        }
                        _ => break,
                    }
                }
            }
        }
        let Some(column) =
            rows[0].pats.iter().position(|p| !matches!(p, Pat::Wild))
        else {
            let row = &rows[0];
            return Expr::Jump(
                row.target.clone(),
                row.params
                    .iter()
                    .map(|p| Expr::Var(row.values[&p.unique()].clone()))
                    .collect(),
                result.clone(),
            );
        };
        let scrut = scruts[column].clone();
        let mut heads = vec![];
        for row in &rows {
            let head = match &row.pats[column] {
                Pat::Con(c, _) => AltCon::Con(c.clone()),
                Pat::Lit(l) => AltCon::Lit(l.clone()),
                _ => continue,
            };
            if !heads.contains(&head) {
                heads.push(head)
            }
        }
        let mut alts = vec![];
        for head in heads {
            let fields = match &head {
                AltCon::Con(c) => self.fields(c, &scrut.ty),
                _ => vec![],
            };
            let params = fields
                .into_iter()
                .map(|t| self.fresh(t))
                .collect::<Vec<_>>();
            let mut specialized = vec![];
            for mut row in rows.clone() {
                let p = row.pats.remove(column);
                let fields = match (&head, p) {
                    (_, Pat::Wild) => vec![Pat::Wild; params.len()],
                    (AltCon::Con(c), Pat::Con(d, ps)) if *c == d => ps,
                    (AltCon::Lit(a), Pat::Lit(b)) if *a == b => vec![],
                    _ => continue,
                };
                row.pats.splice(column..column, fields);
                specialized.push(row)
            }
            let mut columns = scruts.clone();
            columns.splice(column..=column, params.clone());
            let rhs = self.tree(columns, specialized, result, span);
            alts.push(Alt {
                con: head,
                binders: params,
                rhs,
            });
        }
        let mut defaults = vec![];
        for mut row in rows {
            if matches!(row.pats[column], Pat::Wild) {
                row.pats.remove(column);
                defaults.push(row)
            }
        }
        let mut columns = scruts;
        columns.remove(column);
        let fallback = self.tree(columns, defaults, result, span);
        if matches!(scrut.ty, Type::String | Type::Double) {
            let mut body = fallback;
            for alt in alts.into_iter().rev() {
                let AltCon::Lit(l) = alt.con else {
                    unreachable!()
                };
                let primitive = if matches!(scrut.ty, Type::String) {
                    crate::primitives::Prim::StrEq
                } else {
                    crate::primitives::Prim::DblEq
                };
                let test = Expr::Prim(
                    crate::ast::Op::Runtime(primitive),
                    vec![],
                    vec![Expr::Var(scrut.clone()), Expr::Lit(l)],
                    span,
                );
                body = self.boolean(test, alt.rhs, body, result);
            }
            body
        } else {
            alts.push(Alt {
                con: AltCon::Default,
                binders: vec![],
                rhs: fallback,
            });
            Expr::Case(
                Box::new(Expr::Var(scrut.clone())),
                self.fresh(scrut.ty),
                result.clone(),
                alts,
            )
        }
    }
}
fn binders(p: &Pat, out: &mut Vec<Id>) {
    match p {
        Pat::Var(i) => out.push(i.clone()),
        Pat::As(i, p) => {
            out.push(i.clone());
            binders(p, out)
        }
        Pat::Con(_, ps) => {
            for p in ps {
                binders(p, out)
            }
        }
        _ => {}
    }
}
/// Compile every nested match; generated binders come from the shared unique supply.
pub fn compile(
    sess: &mut Session,
    mut program: core::Program,
) -> Result<core::Program, Stop> {
    let mut compiler = Compiler {
        sess,
        constructors: program.constructors.clone(),
        truths: program.groups.iter().flat_map(|g| &g.binds)
            .filter(|b| matches!(&*b.rhs, Expr::Con(c, _, _) if c.name == "True"))
            .map(|b| b.name.unique()).collect(),
    };
    program.groups = program
        .groups
        .into_iter()
        .map(|g| compiler.group(g))
        .collect();
    compiler.sess.finish(program)
}
#[derive(Clone)]
struct Row {
    pats: Vec<Pat>,
    values: BTreeMap<u32, Id>,
    target: names::Id,
    params: Vec<Id>,
}
}

테스트를 만들기 전에 Compiler::compile은 각 절에 매개변수화된 우변 조인을 할당합니다. 또한 그 절 다음에 오는 나머지 행렬을 실패 조인으로 컴파일합니다. 가드는 순서대로 실행됩니다. 모두 실패하면 점프를 통해 그 나머지 행렬로 들어갑니다. 따라서 여러 리프가 절 본문을 복사하지 않고도 하나의 절 본문에 도달할 수 있으며, 실패한 여러 가드가 하나의 폴백을 함께 사용할 수 있습니다.

생성자 분기는 검사 대상(scrutinee)의 실제 타입 인자로 필드를 바인딩해야 합니다. 보조 함수는 선언을 찾아 그 매개변수를 치환합니다:

#![allow(unused)]
fn main() {
fn fields(&self, con: &crate::data::ConId, ty: &Type) -> Vec<Type> {
    let c = &self.constructors[con.index as usize];
    let args = match ty {
        Type::Con(_, args) => args.as_slice(),
        Type::Bool => &[],
        _ => unreachable!("typed data scrutinee"),
    };
    c.fields
        .iter()
        .map(|t| crate::elaborate::instantiate(t, &c.parameters, args))
        .collect()
}
}

예를 들어 Just를 Maybe Int에서 특수화하면 선언의 인스턴스화되지 않은 타입 변수가 아니라 Int를 바인딩합니다. 같은 규칙이 레코드 프로젝션에도 중요합니다: 레코드 프로젝션의 단일 생성자 매치는 이제 태그 분기와 일반 필드 바인더가 됩니다. Core 린트는 이렇게 인스턴스화된 타입을 독립적으로 검사합니다.

Compiler::tree는 행렬을 재귀적으로 처리합니다. 빈 행렬은 PatternFail이 됩니다. 반박 가능한 패턴이 없는 첫 행은 바인더에 대해 누적된 값을 넘기며 해당 절의 조인으로 점프합니다. 그렇지 않으면 선택된 열은 특수화된 하위 트리들과 기본 하위 트리를 갖춘 평평한 Case를 만들거나, 문자열 및 Double 리터럴의 경우 동등성 테스트를 만듭니다.

경고 패스는 원래의 중첩 패턴을 그 표현이 사라지기 전에 사용합니다. 이 패스의 특수화 헬퍼는 앞에서 따라가 본 행렬 연산입니다:

#![allow(unused)]
fn main() {
use crate::{
    core::{ConDecl, Lit, Match, Pat, RowBody, Type},
    diagnostics::{Diagnostic, ErrorCode, Severity},
    session::Session,
};
fn peel(p: &Pat) -> &Pat {
    if let Pat::As(_, p) = p { peel(p) } else { p }
}
fn wild(p: &Pat) -> bool {
    matches!(peel(p), Pat::Wild | Pat::Var(_))
}
fn same(a: &Pat, b: &Pat) -> bool {
    match (peel(a), peel(b)) {
        (Pat::Con(a, _), Pat::Con(b, _)) => a == b,
        (Pat::Lit(Lit::Double(a)), Pat::Lit(Lit::Double(b))) => {
            f64::from_bits(*a) == f64::from_bits(*b)
        }
        (Pat::Lit(a), Pat::Lit(b)) => a == b,
        _ => false,
    }
}
fn fields(head: &Pat, ty: &Type, cons: &[ConDecl]) -> Vec<Type> {
    match peel(head) {
        Pat::Con(id, _) => {
            let c = &cons[id.index as usize];
            let args = match ty {
                Type::Con(_, args) => args.as_slice(),
                _ => &[],
            };
            c.fields
                .iter()
                .map(|t| {
                    crate::elaborate::instantiate(t, &c.parameters, args)
                })
                .collect()
        }
        _ => vec![],
    }
}
fn family(ty: &Type, cons: &[ConDecl]) -> Vec<Pat> {
    cons.iter()
        .filter(|c| match (&c.result, ty) {
            (Type::Bool, Type::Bool) => true,
            (Type::Con(a, _), Type::Con(b, _)) => a == b,
            _ => false,
        })
        .map(|c| Pat::Con(c.id.clone(), vec![Pat::Wild; c.fields.len()]))
        .collect()
}
/// Return a witness matched by the query but not by any covering row.
pub fn useful(
    matrix: &[Vec<Pat>],
    query: &[Pat],
    types: &[Type],
    cons: &[ConDecl],
) -> Option<Vec<Pat>> {
    if query.is_empty() {
        return matrix.is_empty().then(Vec::new);
    }
    let q = peel(&query[0]);
    if !wild(q) {
        let mut ts = fields(q, &types[0], cons);
        let n = ts.len();
        ts.extend_from_slice(&types[1..]);
        let mut qs = match q {
            Pat::Con(_, ps) => ps.clone(),
            _ => vec![],
        };
        qs.extend_from_slice(&query[1..]);
        let mut witness =
            useful(&specialize(matrix, q, n), &qs, &ts, cons)?;
        let children = witness.drain(..n).collect();
        let p = match q {
            Pat::Con(c, _) => Pat::Con(c.clone(), children),
            _ => q.clone(),
        };
        witness.insert(0, p);
        return Some(witness);
    }
    let heads = matrix
        .iter()
        .filter(|r| !wild(&r[0]))
        .map(|r| peel(&r[0]).clone())
        .collect::<Vec<_>>();
    let all = family(&types[0], cons);
    if !all.is_empty()
        && all.iter().all(|h| heads.iter().any(|p| same(h, p)))
    {
        for head in all {
            let mut ts = fields(&head, &types[0], cons);
            let n = ts.len();
            ts.extend_from_slice(&types[1..]);
            let mut qs = vec![Pat::Wild; n];
            qs.extend_from_slice(&query[1..]);
            if let Some(mut w) =
                useful(&specialize(matrix, &head, n), &qs, &ts, cons)
            {
                let children = w.drain(..n).collect();
                let Pat::Con(c, _) = head else { unreachable!() };
                w.insert(0, Pat::Con(c, children));
                return Some(w);
            }
        }
        None
    } else {
        let defaults = matrix
            .iter()
            .filter(|r| wild(&r[0]))
            .map(|r| r[1..].to_vec())
            .collect::<Vec<_>>();
        let mut witness =
            useful(&defaults, &query[1..], &types[1..], cons)?;
        let missing = all
            .into_iter()
            .find(|h| !heads.iter().any(|p| same(h, p)))
            .unwrap_or_else(|| {
                for n in 0..=heads.len() {
                    let p = match types[0] {
                        Type::Int => Pat::Lit(Lit::Int(n as i64)),
                        Type::Char => Pat::Lit(Lit::Char(
                            char::from_u32(97 + n as u32).unwrap_or('\0'),
                        )),
                        Type::String => {
                            Pat::Lit(Lit::String("x".repeat(n)))
                        }
                        Type::Double => {
                            Pat::Lit(Lit::Double((n as f64).to_bits()))
                        }
                        _ => return Pat::Wild,
                    };
                    if !heads.iter().any(|h| same(&p, h)) {
                        return p;
                    }
                }
                unreachable!()
            });
        witness.insert(0, missing);
        Some(witness)
    }
}
fn covers(
    body: &RowBody,
    truths: &std::collections::BTreeSet<u32>,
) -> bool {
    match body {
        RowBody::Plain(_) => true,
        RowBody::Guarded(gs) => gs.iter().any(|(g, _)| match g {
            crate::core::Expr::Con(c, _, _) => c.name == "True",
            crate::core::Expr::Global(id) => truths.contains(&id.unique()),
            _ => false,
        }),
    }
}
/// Warnings do not alter the decision tree; unmatched execution still fails at runtime.
pub fn warn(
    sess: &mut Session,
    m: &Match,
    cons: &[ConDecl],
    truths: &std::collections::BTreeSet<u32>,
) {
    let types = m.scruts.iter().map(|i| i.ty.clone()).collect::<Vec<_>>();
    let mut matrix = vec![];
    for (index, row) in m.rows.iter().enumerate() {
        if useful(&matrix, &row.pats, &types, cons).is_none()
            && !(index > 0 && m.rows[index - 1].span == row.span)
        {
            let mut d = Diagnostic::error(
                ErrorCode::Redundant,
                "clause can never match",
                row.span,
            );
            d.severity = Severity::Warning;
            sess.emit(d);
        }
        if covers(&row.body, truths) {
            matrix.push(row.pats.clone())
        }
    }
    let mut missing = vec![];
    for _ in 0..3 {
        let Some(p) =
            useful(&matrix, &vec![Pat::Wild; types.len()], &types, cons)
        else {
            break;
        };
        missing.push(
            p.iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(" "),
        );
        matrix.push(p)
    }
    if !missing.is_empty() {
        let mut d = Diagnostic::error(
            ErrorCode::NonExhaustive,
            "non-exhaustive pattern match",
            m.span,
        );
        d.severity = Severity::Warning;
        d.notes.push(format!("missing: {}", missing.join("; ")));
        sess.emit(d)
    }
}
fn specialize(
    matrix: &[Vec<Pat>],
    head: &Pat,
    arity: usize,
) -> Vec<Vec<Pat>> {
    matrix
        .iter()
        .filter_map(|row| {
            let mut fields = if wild(&row[0]) {
                vec![Pat::Wild; arity]
            } else if same(&row[0], head) {
                match peel(&row[0]) {
                    Pat::Con(_, ps) => ps.clone(),
                    _ => vec![],
                }
            } else {
                return None;
            };
            fields.extend_from_slice(&row[1..]);
            Some(fields)
        })
        .collect()
}
}

useful은 행렬과 함께 후보 벡터와 열 타입을 지니고 다닙니다. 이 패스의 드라이버는 유용한 증인(witness)을 반복해서 요청하고 진단을 기록합니다:

#![allow(unused)]
fn main() {
use crate::{
    core::{ConDecl, Lit, Match, Pat, RowBody, Type},
    diagnostics::{Diagnostic, ErrorCode, Severity},
    session::Session,
};
fn peel(p: &Pat) -> &Pat {
    if let Pat::As(_, p) = p { peel(p) } else { p }
}
fn wild(p: &Pat) -> bool {
    matches!(peel(p), Pat::Wild | Pat::Var(_))
}
fn same(a: &Pat, b: &Pat) -> bool {
    match (peel(a), peel(b)) {
        (Pat::Con(a, _), Pat::Con(b, _)) => a == b,
        (Pat::Lit(Lit::Double(a)), Pat::Lit(Lit::Double(b))) => {
            f64::from_bits(*a) == f64::from_bits(*b)
        }
        (Pat::Lit(a), Pat::Lit(b)) => a == b,
        _ => false,
    }
}
fn specialize(
    matrix: &[Vec<Pat>],
    head: &Pat,
    arity: usize,
) -> Vec<Vec<Pat>> {
    matrix
        .iter()
        .filter_map(|row| {
            let mut fields = if wild(&row[0]) {
                vec![Pat::Wild; arity]
            } else if same(&row[0], head) {
                match peel(&row[0]) {
                    Pat::Con(_, ps) => ps.clone(),
                    _ => vec![],
                }
            } else {
                return None;
            };
            fields.extend_from_slice(&row[1..]);
            Some(fields)
        })
        .collect()
}
fn fields(head: &Pat, ty: &Type, cons: &[ConDecl]) -> Vec<Type> {
    match peel(head) {
        Pat::Con(id, _) => {
            let c = &cons[id.index as usize];
            let args = match ty {
                Type::Con(_, args) => args.as_slice(),
                _ => &[],
            };
            c.fields
                .iter()
                .map(|t| {
                    crate::elaborate::instantiate(t, &c.parameters, args)
                })
                .collect()
        }
        _ => vec![],
    }
}
fn family(ty: &Type, cons: &[ConDecl]) -> Vec<Pat> {
    cons.iter()
        .filter(|c| match (&c.result, ty) {
            (Type::Bool, Type::Bool) => true,
            (Type::Con(a, _), Type::Con(b, _)) => a == b,
            _ => false,
        })
        .map(|c| Pat::Con(c.id.clone(), vec![Pat::Wild; c.fields.len()]))
        .collect()
}
/// Return a witness matched by the query but not by any covering row.
pub fn useful(
    matrix: &[Vec<Pat>],
    query: &[Pat],
    types: &[Type],
    cons: &[ConDecl],
) -> Option<Vec<Pat>> {
    if query.is_empty() {
        return matrix.is_empty().then(Vec::new);
    }
    let q = peel(&query[0]);
    if !wild(q) {
        let mut ts = fields(q, &types[0], cons);
        let n = ts.len();
        ts.extend_from_slice(&types[1..]);
        let mut qs = match q {
            Pat::Con(_, ps) => ps.clone(),
            _ => vec![],
        };
        qs.extend_from_slice(&query[1..]);
        let mut witness =
            useful(&specialize(matrix, q, n), &qs, &ts, cons)?;
        let children = witness.drain(..n).collect();
        let p = match q {
            Pat::Con(c, _) => Pat::Con(c.clone(), children),
            _ => q.clone(),
        };
        witness.insert(0, p);
        return Some(witness);
    }
    let heads = matrix
        .iter()
        .filter(|r| !wild(&r[0]))
        .map(|r| peel(&r[0]).clone())
        .collect::<Vec<_>>();
    let all = family(&types[0], cons);
    if !all.is_empty()
        && all.iter().all(|h| heads.iter().any(|p| same(h, p)))
    {
        for head in all {
            let mut ts = fields(&head, &types[0], cons);
            let n = ts.len();
            ts.extend_from_slice(&types[1..]);
            let mut qs = vec![Pat::Wild; n];
            qs.extend_from_slice(&query[1..]);
            if let Some(mut w) =
                useful(&specialize(matrix, &head, n), &qs, &ts, cons)
            {
                let children = w.drain(..n).collect();
                let Pat::Con(c, _) = head else { unreachable!() };
                w.insert(0, Pat::Con(c, children));
                return Some(w);
            }
        }
        None
    } else {
        let defaults = matrix
            .iter()
            .filter(|r| wild(&r[0]))
            .map(|r| r[1..].to_vec())
            .collect::<Vec<_>>();
        let mut witness =
            useful(&defaults, &query[1..], &types[1..], cons)?;
        let missing = all
            .into_iter()
            .find(|h| !heads.iter().any(|p| same(h, p)))
            .unwrap_or_else(|| {
                for n in 0..=heads.len() {
                    let p = match types[0] {
                        Type::Int => Pat::Lit(Lit::Int(n as i64)),
                        Type::Char => Pat::Lit(Lit::Char(
                            char::from_u32(97 + n as u32).unwrap_or('\0'),
                        )),
                        Type::String => {
                            Pat::Lit(Lit::String("x".repeat(n)))
                        }
                        Type::Double => {
                            Pat::Lit(Lit::Double((n as f64).to_bits()))
                        }
                        _ => return Pat::Wild,
                    };
                    if !heads.iter().any(|h| same(&p, h)) {
                        return p;
                    }
                }
                unreachable!()
            });
        witness.insert(0, missing);
        Some(witness)
    }
}
fn covers(
    body: &RowBody,
    truths: &std::collections::BTreeSet<u32>,
) -> bool {
    match body {
        RowBody::Plain(_) => true,
        RowBody::Guarded(gs) => gs.iter().any(|(g, _)| match g {
            crate::core::Expr::Con(c, _, _) => c.name == "True",
            crate::core::Expr::Global(id) => truths.contains(&id.unique()),
            _ => false,
        }),
    }
}
/// Warnings do not alter the decision tree; unmatched execution still fails at runtime.
pub fn warn(
    sess: &mut Session,
    m: &Match,
    cons: &[ConDecl],
    truths: &std::collections::BTreeSet<u32>,
) {
    let types = m.scruts.iter().map(|i| i.ty.clone()).collect::<Vec<_>>();
    let mut matrix = vec![];
    for (index, row) in m.rows.iter().enumerate() {
        if useful(&matrix, &row.pats, &types, cons).is_none()
            && !(index > 0 && m.rows[index - 1].span == row.span)
        {
            let mut d = Diagnostic::error(
                ErrorCode::Redundant,
                "clause can never match",
                row.span,
            );
            d.severity = Severity::Warning;
            sess.emit(d);
        }
        if covers(&row.body, truths) {
            matrix.push(row.pats.clone())
        }
    }
    let mut missing = vec![];
    for _ in 0..3 {
        let Some(p) =
            useful(&matrix, &vec![Pat::Wild; types.len()], &types, cons)
        else {
            break;
        };
        missing.push(
            p.iter()
                .map(ToString::to_string)
                .collect::<Vec<_>>()
                .join(" "),
        );
        matrix.push(p)
    }
    if !missing.is_empty() {
        let mut d = Diagnostic::error(
            ErrorCode::NonExhaustive,
            "non-exhaustive pattern match",
            m.span,
        );
        d.severity = Severity::Warning;
        d.notes.push(format!("missing: {}", missing.join("; ")));
        sess.emit(d)
    }
}
}

생성된 do-패턴 폴백 행은 원래 위치의 span을 공유합니다. 진단 패스는 이렇게 합성된 인접 행에 대한 중복 경고는 억제하되, 소스 절에 대한 경고는 유지합니다. 경고 분석이 런타임 분기를 선택하지는 않습니다. 경고에서 생긴 실수가 평가를 조용히 바꾸어서는 안 됩니다.

이제 Core 린트는 평탄한 대안의 모양, 생성자 필드 타입, 전역적으로 고유한 바인더, 그리고 조인 인자의 타입과 항수를 검사합니다. 점프는 자신의 조인에 대해 꼬리 위치에 있어야 합니다. 꼬리가 아닌 하위 표현식에 들어가면 사용 가능한 바깥쪽 조인이 비워집니다. 하위 표현식은 자신만의 지역 조인을 세울 수 있습니다. Core 인터프리터는 평가 프레임과 반환 프레임을 명시적인 벡터에 보관합니다. 소스의 호출은 Rust를 재귀적으로 호출하는 대신 현재 표현식과 환경을 바꿉니다. 점프는 프레임을 자신의 어휘적 조인까지 되감고 그 본문으로 제어를 넘깁니다. 클로저는 안정적인 인덱스를 통해 코드를 빌려 오므로, 함수를 호출해도 구문 트리를 복제하지 않습니다. 생성자와 환경의 해제에도 작업 목록을 사용하므로, 긴 리스트를 해제할 때 스택이 넘치지 않습니다. 테스트는 꼬리가 아닌 재귀와 리스트 회수를 256 KiB 호스트 스택에서 실행합니다.

저장된 환경들은 불변 균형 트리를 공유합니다. 스코프를 포착할 때는 Rc 하나만 복사하고, 바인딩을 추가할 때는 탐색 경로만 복사합니다. env.rs는 이 갱신을 작업 벡터로 구현합니다. 이렇게 하면 인터프리터가 컨티뉴에이션을 저장할 때마다 커져 가는 스코프 전체가 복사되는 일을 막을 수 있습니다. 재귀 클로저는 머신이 소유한 그룹을 인덱스로 참조하므로, 호출은 참조 카운트 순환을 만들지 않고 그룹을 재사용합니다.

실행해 보기

$ cd crates/ch16_match
$ cargo run -q -- run tests/programs/match/guard-fallback.fun --lint
42
$ cargo run -q -- dump --pass=types tests/programs/match/guard-fallback.fun
main : IO ()

변환 자체를 보려면 같은 파일을 --pass=core로 덤프한 다음 --pass=match로 덤프하십시오. 전자는 중첩된 Match를 그대로 유지합니다. 후자는 평평한 Case, 우변 조인, 가드 실패 점프를 드러냅니다. 덤프에는 Prelude가 포함되므로, 결정 트리를 하나씩 살펴보려면 tests/matches.rs에 있는 Prelude 없는 작은 사례들이 유용합니다.

우리는 컴파일 전과 후에 모두 매치를 실행해 보는 방식으로 변환을 검사합니다. 커버리지는 별도의 검사가 필요합니다. 작은 불리언 패턴에 대해서는 가능한 모든 입력을 열거하고, 그렇게 직접 얻은 답을 유용성 알고리즘의 결과와 비교할 수 있습니다. 이는 잘못된 런타임 분기와는 다른 종류의 실수를 잡아냅니다.

연습 문제

  1. 기초. 예제의 결정 트리를 그리고, 실패한 가드가 향하는 곳을 표시하십시오.
  2. 중급. 생성자 절 앞에 와일드카드 절을 추가하고, 검사기를 실행하기 전에 중복 절 경고를 예측해 보십시오.
  3. 중급. 첫 번째 행이 변수로 시작하고 두 번째 패턴이 생성자인 2열 행렬을 특수화하십시오.
  4. 고급. 실험용 사본에서 열 선택 휴리스틱을 바꾸십시오. 변경 전후의 전체 실행 코퍼스는 그대로 유지하면서 트리 크기를 비교하십시오.
  5. 심화. 꼬리 위치가 아닌 식에서 바깥쪽 조인으로 점프하는 잘못된 Core를 만들어 보고, 그 때문에 버려지게 될 컨티뉴에이션이 무엇인지 설명하십시오.

더 읽을거리

같은 행렬 연산을 독립적인 Rust 구현으로 다룬 자료는 Patternkit을 참고하십시오. Fun은 검사 알고리즘이 이 장에서 계속 드러나도록 구현을 장 안에 둡니다.

17장: Prelude와 deriving

새 파일

  • src/derive.rs

변경된 파일

  • src/primitives.rs
  • src/desugar.rs
  • src/ast.rs
  • src/core_lint.rs
  • src/eval.rs
  • src/lexer.rs
  • src/lib.rs
  • src/main.rs
  • src/rename.rs

동기

map, foldr, maybe를 매번 직접 작성하지 않고도 꺼내 쓸 수 있을 때, 언어는 비로소 언어답게 느껴지기 시작합니다. 이제 우리에게는 Fun의 표준 라이브러리를 Fun으로 작성할 수 있을 만큼의 Fun이 갖춰졌습니다. 좋은 이정표입니다!

또한 반복적인 부분은 컴파일러가 작성하도록 가르칠 것입니다. 데이터 타입이 주어지면 deriving은 동등성, 순서, 표시를 위한 평범한 인스턴스를 생성합니다. 생성된 코드는 우리가 손으로 작성한 코드와 똑같은 검사기를 거칩니다. 인터프리터 안에 이 코드를 구해 줄 비밀 동등성 연산이 기다리고 있는 것은 아닙니다.

이 장에서는 인터프리터로 실행되는 언어를 하나로 완성합니다. 백엔드로 넘어가기 전에 잠시 시간을 내어 이 언어를 가지고 놀아 보십시오. 작은 라이브러리 함수는 컴파일러 예제로도 훌륭합니다. 그 타입과 정의만으로도 단 몇 줄 안에 전체 이야기를 들려주는 경우가 많습니다.

현재의 언어

데이터 타입은 deriving Eq를 요청하거나, Eq, Ord, Show 중에서 고른 비어 있지 않은 괄호 목록을 요청할 수 있습니다. 이 예제는 레코드 필드를 선언과 다른 순서로 생성합니다:

data P a = P {x :: a, y :: Int} deriving (Eq, Ord, Show)

main = print (Just (P {y=2,x=Left 1} :: P (Either Int Bool)))

파생된 동등성은 먼저 생성자를 비교하고 그다음 필드를 비교합니다. 순서 비교는 생성자 선언 순서를 따르고, 그다음으로 사전식 필드 순서를 따릅니다. 파생된 Show는 생성 순서와 관계없이 레코드 필드를 선언 순서대로 출력하며, 필요한 곳에서는 중첩된 생성자 적용을 괄호로 감쌉니다. 리스트는 쉼표로 구분된 대괄호 표기를 사용합니다. [Char]는 여전히 문자열이 아니라 리스트입니다. 함수에는 파생된 비교 인스턴스나 표시 인스턴스가 없습니다.

완성된 Prelude는 리스트 폴드와 순회, 유한 생성, 문자열 분할과 결합, 수치 변환, 문자 연산, 모나드 제어를 제공합니다. [a..b]는 양 끝을 포함하는 Int 범위입니다. iterateN은 길이가 제한된 수열을 만듭니다. 생성자 필드가 엄격하기 때문에 무한 리스트는 사용할 수 없습니다. 여기서는 foldl'과 foldl이 똑같이 엄격하게 동작합니다.

리스트를 이어 붙일 때는 ++를, 문자열을 이어 붙일 때는 <>를 사용하십시오. 수치 리터럴은 여전히 고정된 타입을 가지므로 sum과 product는 Int 리스트에 대해 동작합니다. 이 함수들의 시작값은 정수 리터럴이며, Fun의 Num 클래스에는 리터럴을 임의의 수치 타입으로 바꾸는 연산이 없습니다.

작은 함수들을 조합하기

이제 한숨에 읽을 수 있을 만큼 간결한 코드를 작성할 수 있습니다.

total = sum . map (\x -> x * x) . filter even

main = print (total [1..10])

이 프로그램은 220을 출력합니다. 합성은 오른쪽에서 왼쪽으로 읽으십시오. 짝수만 남기고, 각각을 제곱한 다음, 모두 더합니다. 각 함수는 익숙한 일 하나만 하고, (.)는 한 함수의 결과를 다음 함수의 입력으로 연결합니다. 루프 인덱스에 이름을 붙이거나 리스트를 어떻게 따라가며 진행할지 설명할 필요가 없습니다.

계산을 표현하는 아름다운 방식입니다. 그렇다고 해서 할 일이 사라진 것은 아닙니다. Fun은 엄격한 언어이며, 이 프로그램은 중간 리스트를 만듭니다. 나중에 최적화를 통해 이 리스트들을 없앨 수도 있지만, 유용한 분리는 이미 여기에 있습니다. 프로그램을 더 빠르게 만들 방법을 고르기 전에 프로그램이 무엇을 의미하는지 먼저 이해할 수 있습니다.

이론

트리의 동등성을 손으로 작성한다고 상상해 보십시오. 빈 트리끼리는 같습니다. 두 노드는 대응하는 필드가 서로 같을 때 같습니다. 서로 다른 생성자는 같지 않습니다. 데이터 타입 선언만으로도 이 절들을 작성하기에 충분한 정보를 알 수 있으므로, 컴파일러가 이 절들을 작성하게 합시다.

그것이 바로 파생(deriving)입니다. 우리는 평범한 인스턴스를 생성한 다음, 손으로 작성한 인스턴스를 검사할 때와 똑같이 그것을 검사합니다. Haskell Report는 이 구조적 동작을 명세합니다 (Simon Marlow 2010). 우리는 이것을 Fun의 레코드, 원시 문자열, 그리고 더 작은 Show 인터페이스에 맞게 조정합니다.

필드에서 쓰이는 매개변수는 보통 그에 대응하는 인스턴스 제약을 만듭니다. 팬텀 매개변수는 그렇지 않습니다. data Token a = Token Int의 경우, a 타입의 값을 전혀 검사하지 않으므로 동등성 비교에 Eq a 사전이 필요하지 않습니다. 어떤 매개변수가 필드에 나타나는지 판단하기 전에 타입 동의어를 먼저 전개해야 합니다. 그런 다음 일반 인스턴스 솔버가 생성된 컨텍스트가 모든 필드 연산에 실제로 충분한지 검사합니다 (Jones 1999). 도출(deriving)이 이러한 검사를 피해 가는 수단은 아닙니다.

생성자를 출력하는 데에는 작지만 중요한 까다로운 점이 하나 있습니다. Just (Left 3)에는 괄호가 필요하지만, Left 3 단독으로는 필요하지 않습니다. a -> String 타입의 함수는 자신의 결과가 어디에서 쓰일지 알 수 없습니다. 따라서 그 주변의 우선순위도 함께 넘겨주어야 합니다.

Fun의 Show 사전에는 이를 위한 내부 메서드 _showsPrec이 있습니다. 직접 작성한 인스턴스는 기본 구현을 물려받고, 파생 인스턴스는 이를 구조적으로 구현합니다. 최상위 show는 우선순위 0에서 시작합니다. 위치 기반 생성자 인자는 11을 사용하는데, 이는 중첩된 생성자 적용이 스스로 괄호를 치도록 요구합니다. 레코드 필드는 레이블이 붙은 중괄호 안에서 일반 show를 사용합니다.

엄격성은 익숙한 라이브러리 정의를 하나 더 바꿉니다. 일반적인 최상위 정의 undefined = error "undefined"는 쓰이지 않더라도 시작 시점에 실패합니다. 그 대신, 가려지지 않은 Prelude의 undefined 참조는 유닛을 받는 보조 함수를 적용하는 식의 범위 한정 약어입니다. 이렇게 하면 썽크나 지연 바인딩을 도입하지 않고도 사용 지점에서 실패하는 동작을 유지할 수 있습니다. 5장에서 다룬 평가 전략의 구분은 여전히 핵심적입니다(Pierce 2002).

구현

derive::derive는 클래스 수집과 어휘적 이름 변경 직전에 실행됩니다. 생성된 항은 데이터 타입의 소스 스팬을 그대로 유지하므로, 거부된 필드 연산이 있으면 그 선언을 가리킬 수 있습니다. 이 패스는 사용된 매개변수를 수집할 때 별칭을 전개하고, 세 가지 생성기 중 하나를 선택한 뒤, 일반 인스턴스 선언을 덧붙입니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::*,
    diagnostics::{Diagnostic, ErrorCode},
    session::{Session, Stop},
    source::Span,
};
use std::collections::BTreeSet;
struct Builder {
    span: Span,
}
impl Builder {
    fn expr(&self, kind: Kind) -> Expr {
        Expr {
            kind,
            span: self.span,
        }
    }
    fn var(&self, name: &str) -> Expr {
        self.expr(Kind::Var(name.into()))
    }
    fn string(&self, text: &str) -> Expr {
        self.expr(Kind::String(text.into()))
    }
    fn call(&self, name: &str, args: Vec<Expr>) -> Expr {
        args.into_iter().fold(self.var(name), |f, x| {
            self.expr(Kind::App(Box::new(f), Box::new(x)))
        })
    }
    fn pat(&self, name: &str) -> Pat {
        Pat {
            kind: PatKind::Var(name.into()),
            span: self.span,
        }
    }
    fn constructor(&self, c: &ConstructorDecl, prefix: &str) -> Pat {
        Pat {
            kind: PatKind::Con(
                c.name.clone(),
                (0..c.fields.len())
                    .map(|i| self.pat(&format!("{prefix}{i}")))
                    .collect(),
            ),
            span: self.span,
        }
    }
    fn row(&self, patterns: Vec<Pat>, expr: Expr) -> Row {
        Row {
            patterns,
            body: RowBody::Plain(Box::new(expr)),
            locals: vec![],
            span: self.span,
        }
    }
    fn binding(&self, name: &str, rows: Vec<Row>) -> Binding {
        Binding {
            name: name.into(),
            rhs: self.expr(Kind::Equations(rows)),
            nonrecursive: vec![],
            span: self.span,
        }
    }
    fn append(&self, pieces: Vec<Expr>) -> Expr {
        pieces
            .into_iter()
            .reduce(|a, b| self.call("<>", vec![a, b]))
            .unwrap_or_else(|| self.string(""))
    }
    fn eq(&self, d: &DataDecl) -> Binding {
        let mut rows = vec![];
        for (i, a) in d.constructors.iter().enumerate() {
            for (j, b) in d.constructors.iter().enumerate() {
                let mut rhs =
                    self.var(if i == j { "True" } else { "False" });
                if i == j {
                    for k in (0..a.fields.len()).rev() {
                        let test = self.call(
                            "==",
                            vec![
                                self.var(&format!("x{k}")),
                                self.var(&format!("y{k}")),
                            ],
                        );
                        rhs = self.expr(Kind::If(
                            Box::new(test),
                            Box::new(rhs),
                            Box::new(self.var("False")),
                        ));
                    }
                }
                rows.push(self.row(
                    vec![
                        self.constructor(a, "x"),
                        self.constructor(b, "y"),
                    ],
                    rhs,
                ));
            }
        }
        self.binding("==", rows)
    }
    fn ord(&self, d: &DataDecl) -> Binding {
        let mut rows = vec![];
        for (i, a) in d.constructors.iter().enumerate() {
            for (j, b) in d.constructors.iter().enumerate() {
                let mut rhs = self.var(if i < j {
                    "LT"
                } else if i > j {
                    "GT"
                } else {
                    "EQ"
                });
                if i == j {
                    for k in (0..a.fields.len()).rev() {
                        let comparison = self.call(
                            "compare",
                            vec![
                                self.var(&format!("x{k}")),
                                self.var(&format!("y{k}")),
                            ],
                        );
                        let equal = Pat {
                            kind: PatKind::Con("EQ".into(), vec![]),
                            span: self.span,
                        };
                        rhs = self.expr(Kind::Case(
                            Box::new(comparison),
                            vec![
                                self.row(vec![equal], rhs),
                                self.row(
                                    vec![self.pat("order")],
                                    self.var("order"),
                                ),
                            ],
                        ));
                    }
                }
                rows.push(self.row(
                    vec![
                        self.constructor(a, "x"),
                        self.constructor(b, "y"),
                    ],
                    rhs,
                ));
            }
        }
        self.binding("compare", rows)
    }
    fn show(&self, d: &DataDecl) -> Vec<Binding> {
        let mut rows = vec![];
        for c in &d.constructors {
            let mut pieces = vec![self.string(&c.name)];
            if let Some(labels) = &c.labels {
                pieces.push(self.string(" {"));
                for (i, label) in labels.iter().enumerate() {
                    pieces.push(self.string(&format!(
                        "{}{label} = ",
                        if i == 0 { "" } else { ", " }
                    )));
                    pieces.push(
                        self.call("show", vec![self.var(&format!("x{i}"))]),
                    );
                }
                pieces.push(self.string("}"));
            } else {
                for i in 0..c.fields.len() {
                    pieces.push(self.string(" "));
                    pieces.push(self.call(
                        "_showsPrec",
                        vec![
                            self.expr(Kind::Int(11)),
                            self.var(&format!("x{i}")),
                        ],
                    ));
                }
            }
            let mut rhs = self.append(pieces);
            if !c.fields.is_empty() {
                let test = self.call(
                    ">",
                    vec![self.var("p"), self.expr(Kind::Int(10))],
                );
                rhs = self.call("_showParen", vec![test, rhs]);
            }
            rows.push(
                self.row(
                    vec![self.pat("p"), self.constructor(c, "x")],
                    rhs,
                ),
            );
        }
        let show = self.binding(
            "show",
            vec![self.row(
                vec![self.pat("value")],
                self.call(
                    "_showsPrec",
                    vec![self.expr(Kind::Int(0)), self.var("value")],
                ),
            )],
        );
        vec![show, self.binding("_showsPrec", rows)]
    }
}
fn variables(ty: &TypeSyntax, out: &mut BTreeSet<String>) {
    match ty {
        TypeSyntax::Name(n) => {
            out.insert(n.clone());
        }
        TypeSyntax::App(a, b) | TypeSyntax::Arrow(a, b) => {
            variables(a, out);
            variables(b, out);
        }
        TypeSyntax::Qualified(_, t) => variables(t, out),
    }
}
/// Generate only Eq, Ord and Show. Generated terms retain the declaration span.
pub fn derive(
    sess: &mut Session,
    program: &mut Program,
) -> Result<(), Stop> {
    for d in &program.data {
        if d.deriving.is_empty() {
            continue;
        }
        let build = Builder { span: d.span };
        let mut used = BTreeSet::new();
        for c in &d.constructors {
            for ty in &c.fields {
                let expanded =
                    crate::kinds::expand(ty, &program.aliases, d.span)
                        .map_err(|e| {
                            sess.emit(e);
                            Stop
                        })?;
                variables(&expanded, &mut used);
            }
        }
        for class in &d.deriving {
            let methods = match class.as_str() {
                "Eq" => vec![build.eq(d)],
                "Ord" => vec![build.ord(d)],
                "Show" => build.show(d),
                _ => {
                    sess.emit(Diagnostic::error(
                        ErrorCode::Instance,
                        "only Eq, Ord and Show can be derived",
                        d.span,
                    ));
                    return Err(Stop);
                }
            };
            let context = d
                .parameters
                .iter()
                .filter(|p| used.contains(*p))
                .map(|p| PredicateSyntax {
                    class: class.clone(),
                    variable: p.clone(),
                })
                .collect();
            let head = d.parameters.iter().fold(
                TypeSyntax::Name(d.name.clone()),
                |a, p| {
                    TypeSyntax::App(
                        Box::new(a),
                        Box::new(TypeSyntax::Name(p.clone())),
                    )
                },
            );
            program.instances.push(InstanceDecl {
                class: class.clone(),
                head,
                context,
                methods,
                span: d.span,
            });
        }
    }
    Ok(())
}
}

빌더들은 기존의 표면 표현식 타입과 패턴 타입을 구성합니다. 동등성은 생성자 쌍마다 절을 하나씩 생성합니다. 서로 다른 생성자는 False를 반환합니다. 일치하는 생성자는 필드를 왼쪽에서 오른쪽으로 비교하며, 처음으로 같지 않은 쌍에서 멈춥니다:

#![allow(unused)]
fn main() {
fn eq(&self, d: &DataDecl) -> Binding {
    let mut rows = vec![];
    for (i, a) in d.constructors.iter().enumerate() {
        for (j, b) in d.constructors.iter().enumerate() {
            let mut rhs =
                self.var(if i == j { "True" } else { "False" });
            if i == j {
                for k in (0..a.fields.len()).rev() {
                    let test = self.call(
                        "==",
                        vec![
                            self.var(&format!("x{k}")),
                            self.var(&format!("y{k}")),
                        ],
                    );
                    rhs = self.expr(Kind::If(
                        Box::new(test),
                        Box::new(rhs),
                        Box::new(self.var("False")),
                    ));
                }
            }
            rows.push(self.row(
                vec![
                    self.constructor(a, "x"),
                    self.constructor(b, "y"),
                ],
                rhs,
            ));
        }
    }
    self.binding("==", rows)
}
}

순서 비교도 같은 생성자 쌍 구조를 따릅니다. 생성자가 일치하는 쌍에서는 각 필드를 비교하며, 결과가 EQ일 때만 다음 필드로 넘어갑니다. 반면 Show는 생성자 이름, 필드 레이블, 구두점, 그리고 재귀적인 _showsPrec 호출로 문자열 표현식을 만듭니다. 이 식들은 일반적인 이름 변경, 추론, 사전 정교화, 매치 컴파일, Core 린트를 거칩니다. 지원하지 않는 클래스를 요청하면 E0211을 보고합니다. 필요한 동등성 증거 없이 Ord를 요청하는 경우에도 기존 인스턴스 검사를 통해 실패합니다.

라이브러리는 prelude.fun에 있으며, 사용자 프로그램 앞에 내장됩니다. 라이브러리의 비공개 헬퍼 참조는 그 스코프 안에서 해석되므로, 사용자 바인딩이 Prelude 함수의 구현을 몰래 바꿀 수 없습니다. 닫힌 범위(inclusive range)는 값을 증가시키기 전에 상한을 검사합니다. 가장 큰 63비트 정수에서 끝나는 범위는 랩어라운드되지 않고 종료되어야 합니다.

문자 분류와 단순 대소문자 변환은 unicode.fun에 체크인된 유니코드 17.0.0 테이블을 사용합니다. tools/unicode.py는 업스트림 출처와 그 다이제스트를 기록하며, 데이터 라이선스도 함께 보존합니다. 생성된 테이블은 평범하고 간결한 Fun 문자열입니다. 따라서 일반 범주(General Category) 술어와 단순 대소문자 매핑은 새로운 프리미티브를 대거 추가하는 대신 언어 자체를 통해 실행됩니다. isDigit은 의도적으로 ASCII 십진 숫자를 인식합니다. 유니코드에서 숫자로 분류된 문자라고 해서 모두 이를 만족하는 것은 아닙니다.

라이브러리는 컴파일러가 어디서 불필요한 일을 하고 있는지도 알려 줍니다. 리스트를 순회할 때 매 단계마다 꼬리 전체를 복사해서는 안 됩니다. 우리는 불변 생성자 필드 벡터를 Rc로 공유하며, Core 린트는 환경 전체를 복사하는 대신 스코프를 벗어날 때 지역 바인딩을 제거합니다. 이는 Fun 프로그램뿐 아니라 우리 Rust 표현의 비용도 살펴봐야 한다는 점을 일깨워 주는 유용한 사례입니다.

실행해 보기

$ cd crates/ch17_prelude
$ cargo run -q -- run tests/programs/prelude/derive-show-record.fun --lint
Just (P {x = Left 1, y = 2})
$ cargo run -q -- dump --pass=types tests/programs/prelude/derive-show-record.fun
main : IO ()

출력은 선언 순서대로 나열된 레코드 필드와 Just 아래에 놓인 레코드 생성자를 감싸는 괄호를 모두 보여 줍니다. --pass=core는 생성된 인스턴스 사전을 보여 줍니다. --pass=match는 그 사전들의 컴파일된 구조 검사를 보여 줍니다. 별도의 derive-show-tree와 derive-show-literals 픽스처는 중첩된 적용과 이스케이프 처리를 다룹니다.

라이브러리의 경계 사례는 좋은 실험 거리가 됩니다. 가장 큰 Int에서의 단일 원소 범위, fold에 전달된 빈 리스트, 팬텀 매개변수가 있는 파생 인스턴스를 시험해 보십시오. 여기서는 작은 예제들이 실제로 중요한 역할을 합니다. 경계 사례 하나가 길고 평범한 입력보다 라이브러리 정의를 더 명확하게 설명해 줄 수 있습니다.

연습 문제

  1. 준비 운동. 생성자가 두 개인 트리에 대해 Eq, Ord, Show를 파생하고, 그 결과를 직접 작성한 인스턴스와 비교하십시오.
  2. 중급. 사용하지 않는 데이터 타입 매개변수를 추가하고, 그 매개변수의 사전이 파생 인스턴스 문맥에 나타나는지 살펴보십시오.
  3. 중급. Just (Left (Just 3))을 표시하는 동안 전달되는 우선순위를 추적하십시오.
  4. 중급. 최대 Int에서의 단일 원소 범위를 테스트하고, 상한 검사와 증가의 순서가 왜 중요한지 설명하십시오.
  5. 심화. Fun 프로그램은 바꾸지 말고, 불변 필드 벡터 공유를 도입하기 전과 후에 유니코드 조회에서 일어나는 생성자 꼬리 복사를 측정해 보십시오.

더 읽을거리

18장: A-정규형과 조인 포인트

새 파일

  • src/erase.rs
  • src/anf.rs
  • src/anf_simplify.rs
  • src/anf_machine.rs
  • src/anf_lint.rs

변경된 파일

  • src/lib.rs
  • src/main.rs

동기

f (g x) (h y)를 생각해 봅시다. 이것을 실행하려면 함수와 인자들을 올바른 순서로 평가하고, h y를 계산하는 동안 g x의 결과를 보관해 두었다가, 마지막으로 호출을 수행해야 합니다. 지금까지는 우리 인터프리터가 이런 장부 정리를 대신 해 주었습니다. 코드 생성기는 그 단계들을 직접 볼 수 있어야 합니다.

흔히 ANF로 줄여 부르는 A-정규형은 이러한 중간 결과들을 이름 붙은 바인딩으로 적어 둡니다. 그러면 함수 호출과 다른 연산들은 중첩된 계산 대신 단순한 피연산자를 받게 됩니다. 쓸모 있는 지역 변수 몇 개를 도입하는 것에 불과한 일치고는 이름이 꽤 거창하게 들립니다.

이는 5장의 CEK 기계를 다시 불러오기에도 좋은 지점입니다. 이제 Fun의 데이터, 사전, IO, 조인 포인트를 모두 갖춘 ANF를 명시적인 컨티뉴에이션 스택으로 실행하겠습니다. 프로그램을 네이티브 코드에 맞게 준비하면서 그 결과를 Core와 비교할 수 있습니다.

현재의 언어

결과가 다른 계산에 쓰이는 case 식을 따라가 봅시다.

main = print (3 + (case Just 4 of { Just x -> x; Nothing -> 0 }))

어느 대안이 값을 반환하든, 프로그램은 3을 더하고 한 번 출력해야 합니다. ANF는 그 공통 컨티뉴에이션을 각 대안에 복사하는 대신 조인으로 명시합니다. 새로 추가된 --engine=anf는 반복적 기계를 선택하며 기본값입니다. --engine=core는 참조 인터프리터를 유지합니다.

--no-simplify는 선택적 재작성이 적용되기 전에 생성된 ANF를 보여 줍니다. 두 형태 모두 stdout, stderr의 첫 줄, 프로세스 상태를 보존해야 합니다. 특히 엄격한 평가에서는 쓰이지 않는 바인딩의 초기화식도 여전히 실행됩니다. 실패하는 나눗셈은 그 결과가 쓰이지 않는다는 이유만으로 사라질 수 없습니다.

이론

f (g x) (h y)에서 중요한 작업은 호출에 있습니다. 이름 자체는 값을 가리킬 뿐입니다. ANF는 이 구분을 구문의 일부로 만듭니다. 아톰은 리터럴이거나 사용 가능한 값에 대한 참조입니다. 우변은 아톰에 대해 연산 하나를 수행하고, Let은 그 결과에 이름을 붙입니다. 중첩된 평가가 명시적인 순서로 바뀐 것입니다(Flanagan et al. 1993).

변환은 결과를 어떻게 처리할지 기술하는 문맥을 받습니다. 함수의 끝에서는 아톰을 반환합니다. 식 내부에서는 아톰을 바인딩하고 계속 진행합니다. case 대안의 끝에서는 공유된 컨티뉴에이션으로 점프할 수 있습니다. 이 표현은 Maurer와 동료들이 논의한 조인 포인트의 동기를 따라 (Maurer et al. 2017), 조인을 지역적인 2급 클래스(second-class) 구성 요소로 유지합니다. 모든 함수를 컨티뉴에이션을 명시적으로 받는 함수로 바꾸지는 않습니다.

타입 소거가 정당화되는 근거는 System F의 타입 추상화와 타입 적용이 하는 역할에 있습니다. 이들은 계산을 선택하고 기술하지만 그 자체로 런타임 작업을 수행하지는 않습니다(Pierce 2002). 사전 인자는 다릅니다. 딕셔너리 인자는 실제 메서드 구현을 담고 있으므로 일반 값으로 남아 있어야 합니다. IO는 소거 이전에 이미 unit 함수 표현을 갖추고 있습니다.

꼬리 위치는 남은 작업의 속성입니다. 함수 호출이 현재 함수의 결과를 직접 제공한다면, 기계는 그 활성화를 위한 반환 프레임이 필요하지 않습니다. 꼬리 호출이 아닌 호출은 보류 중인 계산을 기억해야 합니다. 따라서 호스트 스택 사용량이 일정하다고 해서 모든 재귀 프로그램이 일정한 메모리를 사용한다는 뜻은 아닙니다. 그래도 대기 중인 비꼬리 작업 때문에 명시적인 컨티뉴에이션 스택이 커질 수는 있습니다. 이것이 CEK 컨티뉴에이션을 눈에 보이게 만드는 것의 실질적인 이점입니다 (Felleisen and Friedman 1986).

구현

erase::erase는 실행 가능한 타입 정보를 제거하면서 전역 초기화 순서를 보존합니다. 후위 순회 작업 목록은 TyLam과 TyApp을 버리고, 사전 항은 유지하며, 이전 패스에서 빠져나온 중첩된 Match가 있으면 거부합니다. 읽기 전용 생성자 메타데이터와 위치 메타데이터는 프리미티브, 프린터, 진단에서 계속 사용할 수 있습니다.

#![allow(unused)]
fn main() {
use crate::{ast::Op, core, data::ConId, names::Id, source::Span};
#[derive(Clone, Debug)]
/// Untyped Core retains strict evaluation order and lexical join points.
pub enum Expr {
    Var(Id),
    Global(Id),
    Lit(core::Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(bool, Vec<(Id, Expr)>, Box<Expr>),
    Con(ConId, Vec<Expr>),
    Prim(Op, Vec<Expr>, Span),
    Case(Box<Expr>, Id, Vec<(core::AltCon, Vec<Id>, Expr)>),
    Join(Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(Id, Vec<Expr>),
    Fail(Span),
}
#[derive(Clone, Debug)]
/// Ordered globals and runtime constructor/location metadata.
pub struct Program {
    pub globals: Vec<(Id, Expr)>,
    pub metadata: core::Program,
}
/// Postorder erasure uses an explicit worklist, including long constructor spines.
fn expr(root: &core::Expr) -> Expr {
    use core::Expr as C;
    let mut todo = vec![(root, false)];
    let mut results: Vec<Expr> = vec![];
    while let Some((e, done)) = todo.pop() {
        let children: Vec<&core::Expr> = match e {
            C::Lam(_, e) | C::TyLam(_, e) | C::TyApp(e, _) => vec![e],
            C::App(f, a) => vec![f, a],
            C::Let(g, b) => g
                .binds
                .iter()
                .map(|b| b.rhs.as_ref())
                .chain(std::iter::once(b.as_ref()))
                .collect(),
            C::Con(_, _, es) | C::Prim(_, _, es, _) | C::Jump(_, es, _) => {
                es.iter().collect()
            }
            C::Case(e, _, _, alts) => std::iter::once(e.as_ref())
                .chain(alts.iter().map(|a| &a.rhs))
                .collect(),
            C::Join(_, _, b, r) => vec![b, r],
            C::Match(_) => panic!("match compilation precedes erasure"),
            _ => vec![],
        };
        if !done {
            todo.push((e, true));
            todo.extend(children.into_iter().rev().map(|c| (c, false)));
            continue;
        }
        let start = results.len() - children.len();
        let mut parts = results.split_off(start).into_iter();

        let result = match e {
            C::Var(i) => Expr::Var(i.name.clone()),
            C::Global(i) => Expr::Global(i.name.clone()),
            C::Lit(l) => Expr::Lit(l.clone()),
            C::TyLam(..) | C::TyApp(..) => parts.next().unwrap(),
            C::Lam(i, _) => {
                Expr::Lam(i.name.clone(), Box::new(parts.next().unwrap()))
            }
            C::App(..) => Expr::App(
                Box::new(parts.next().unwrap()),
                Box::new(parts.next().unwrap()),
            ),
            C::Let(g, _) => {
                let bs = g
                    .binds
                    .iter()
                    .map(|b| (b.name.name.clone(), parts.next().unwrap()))
                    .collect();
                Expr::Let(g.recursive, bs, Box::new(parts.next().unwrap()))
            }
            C::Con(c, _, _) => Expr::Con(c.clone(), parts.collect()),
            C::Prim(p, _, _, s) => Expr::Prim(*p, parts.collect(), *s),
            C::Case(_, i, _, alts) => {
                let e = parts.next().unwrap();
                let alts = alts
                    .iter()
                    .map(|a| {
                        (
                            a.con.clone(),
                            a.binders
                                .iter()
                                .map(|i| i.name.clone())
                                .collect(),
                            parts.next().unwrap(),
                        )
                    })
                    .collect();
                Expr::Case(Box::new(e), i.name.clone(), alts)
            }
            C::Join(j, ps, _, _) => Expr::Join(
                j.clone(),
                ps.iter().map(|i| i.name.clone()).collect(),
                Box::new(parts.next().unwrap()),
                Box::new(parts.next().unwrap()),
            ),
            C::Jump(j, _, _) => Expr::Jump(j.clone(), parts.collect()),
            C::PatternFail(s, _) => Expr::Fail(*s),
            C::Match(_) => {
                unreachable!("match compilation precedes erasure")
            }
        };
        results.push(result);
    }
    results.pop().unwrap()
}
/// Compact syntax uses an explicit print stack and bounded indentation. Debug's
/// indentation grows with depth and makes even a linear tree quadratic in bytes.
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Expr(&'a Expr),
            Text(String),
        }
        let mut todo = vec![Work::Expr(self)];
        while let Some(work) = todo.pop() {
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Expr(e) => {
                    let mut parts = vec![];
                    match e {
                        Expr::Var(v) => write!(f, "{v}")?,
                        Expr::Global(v) => write!(f, "@{v}")?,
                        Expr::Lit(l) => write!(f, "{l}")?,
                        Expr::Lam(v, b) => {
                            write!(f, "(lambda {v}. ")?;
                            parts.push(Work::Expr(b));
                            parts.push(Work::Text(")".into()));
                        }
                        Expr::App(a, b) => {
                            write!(f, "(")?;
                            parts.extend([
                                Work::Expr(a),
                                Work::Text(" ".into()),
                                Work::Expr(b),
                                Work::Text(")".into()),
                            ]);
                        }
                        Expr::Con(c, es) => {
                            write!(f, "({}", c.name)?;
                            for e in es {
                                parts.push(Work::Text(" ".into()));
                                parts.push(Work::Expr(e));
                            }
                            parts.push(Work::Text(")".into()));
                        }
                        Expr::Prim(p, es, _) => {
                            write!(f, "({}", p.text())?;
                            for e in es {
                                parts.push(Work::Text(" ".into()));
                                parts.push(Work::Expr(e));
                            }
                            parts.push(Work::Text(")".into()));
                        }
                        Expr::Jump(j, es) => {
                            write!(f, "(jump {j}")?;
                            for e in es {
                                parts.push(Work::Text(" ".into()));
                                parts.push(Work::Expr(e));
                            }
                            parts.push(Work::Text(")".into()));
                        }
                        Expr::Let(rec, bs, b) => {
                            write!(
                                f,
                                "(let{} ",
                                if *rec { "rec" } else { "" }
                            )?;
                            for (v, e) in bs {
                                parts.push(Work::Text(format!("{v} = ")));
                                parts.push(Work::Expr(e));
                                parts.push(Work::Text(";\n".into()));
                            }
                            parts.extend([
                                Work::Text("in ".into()),
                                Work::Expr(b),
                                Work::Text(")".into()),
                            ]);
                        }
                        Expr::Case(e, v, alts) => {
                            write!(f, "(case ")?;
                            parts.extend([
                                Work::Expr(e),
                                Work::Text(format!(" as {v} {{\n")),
                            ]);
                            for (c, ps, e) in alts {
                                let c = match c {
                                    core::AltCon::Con(c) => c.name.clone(),
                                    core::AltCon::Lit(l) => l.to_string(),
                                    core::AltCon::Default => "_".into(),
                                };
                                parts.push(Work::Text(c));
                                for p in ps {
                                    parts.push(Work::Text(format!(" {p}")));
                                }
                                parts.extend([
                                    Work::Text(" -> ".into()),
                                    Work::Expr(e),
                                    Work::Text(";\n".into()),
                                ]);
                            }
                            parts.push(Work::Text("})".into()));
                        }
                        Expr::Join(j, ps, b, r) => {
                            write!(f, "(join {j}")?;
                            for p in ps {
                                write!(f, " {p}")?;
                            }
                            write!(f, " = ")?;
                            parts.extend([
                                Work::Expr(b),
                                Work::Text("\nin ".into()),
                                Work::Expr(r),
                                Work::Text(")".into()),
                            ]);
                        }
                        Expr::Fail(s) => write!(f, "fail {s:?}")?,
                    }
                    todo.extend(parts.into_iter().rev());
                }
            }
        }
        Ok(())
    }
}
/// Destroy an owned erased tree without recursively following Box edges.
pub fn discard(root: Expr) {
    let mut todo = vec![root];
    while let Some(e) = todo.pop() {
        match e {
            Expr::Lam(_, b) => todo.push(*b),
            Expr::App(a, b) | Expr::Join(_, _, a, b) => {
                todo.push(*a);
                todo.push(*b);
            }
            Expr::Let(_, bs, b) => {
                todo.push(*b);
                todo.extend(bs.into_iter().map(|(_, e)| e));
            }
            Expr::Con(_, es) | Expr::Prim(_, es, _) | Expr::Jump(_, es) => {
                todo.extend(es)
            }
            Expr::Case(e, _, alts) => {
                todo.push(*e);
                todo.extend(alts.into_iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
/// Erase executable types only; read-only metadata supports diagnostics and printers.
pub fn erase(mut program: core::Program) -> Program {
    let globals = program
        .groups
        .iter()
        .flat_map(|g| &g.binds)
        .map(|b| (b.name.name.clone(), expr(&b.rhs)))
        .collect();
    // Consuming Core must not recursively destroy its nested expressions.
    let mut todo = program
        .groups
        .drain(..)
        .flat_map(|g| g.binds.into_iter().map(|b| *b.rhs))
        .collect::<Vec<_>>();
    while let Some(e) = todo.pop() {
        use core::Expr as C;
        match e {
            C::Lam(_, b) | C::TyLam(_, b) | C::TyApp(b, _) => todo.push(*b),
            C::App(a, b) | C::Join(_, _, a, b) => {
                todo.push(*a);
                todo.push(*b);
            }
            C::Let(g, b) => {
                todo.push(*b);
                todo.extend(g.binds.into_iter().map(|b| *b.rhs));
            }
            C::Con(_, _, es) | C::Prim(_, _, es, _) | C::Jump(_, es, _) => {
                todo.extend(es)
            }
            C::Case(e, _, _, alts) => {
                todo.push(*e);
                todo.extend(alts.into_iter().map(|a| a.rhs));
            }
            C::Match(_) => {
                unreachable!("match compilation precedes erasure")
            }
            _ => {}
        }
    }
    Program {
        globals,
        metadata: program,
    }
}
}

ANF 문법

다음은 제어 언어를 기호 형태로 나타낸 것입니다. 아톰에는 를, 우변에는 를, 완전한 ANF 본문에는 를 사용합니다. 각 행은 아래의 Rust 변형 하나에 대응합니다.

윗줄(overbar)은 시퀀스를 뜻합니다. 이름 는 조인 포인트를 식별하고, 는 대안(alternative)의 생성자 태그, 리터럴, 또는 기본(default) 표시입니다. 생성자 대안은 자신의 필드를 에 바인딩합니다. 리터럴 대안과 기본 대안에는 필드 바인더가 없습니다.

더 작은 구성 요소들이 이름의 “정규형” 부분을 설명해 줍니다. 피연산자는 언제나 아톰이며, 아직 일어나지 않은 또 다른 호출인 경우는 결코 없습니다.

여기서 는 생성자를, 는 프리미티브를 가리킵니다. 문법에서는 프리미티브의 소스 스팬을 생략했습니다. Core의 람다와 달리 이 람다들은 여러 매개변수를 받을 수 있으며, 타입 추상화나 타입 적용은 더 이상 남아 있지 않습니다.

이제 일반 호출과 꼬리 호출의 차이가 구문에 드러납니다. 는 호출 뒤에 할 작업을 남겨 둡니다. 에는 그런 본문이 없습니다. 점프 역시 뒤따르는 본문이 없지만, 현재 함수 안에 머무르면서 지역 조인 포인트로 제어를 넘깁니다.

ANF 제어 언어는 일반 바인딩, 재귀 클로저, 분기, 조인, 종결 제어 이전(terminal transfer)을 서로 구분합니다. Fail은 패턴 실패의 소스 스팬을 보존합니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::Op,
    core::{self, AltCon, Lit},
    data::ConId,
    erase,
    names::Id,
    session::Session,
    source::Span,
};
use std::{collections::BTreeMap, rc::Rc};
/// An erased binder keeps its unique identity and diagnostic spelling.
pub type Var = Id;
/// Syntax is immutable and shared between closures and machine states.
pub type Body = Rc<Anf>;
#[derive(Clone, Debug, PartialEq, Eq)]
/// Atoms cannot perform work or fail when read from a well-scoped environment.
pub enum Atom {
    Var(Var),
    Global(Id),
    Lit(Lit),
}
#[derive(Clone, Debug)]
/// Lambda parameters are merged only across syntactically adjacent lambdas.
pub struct Lambda {
    pub params: Vec<Var>,
    pub body: Body,
}
#[derive(Clone, Debug)]
/// One strict computation whose operands have already been evaluated.
pub enum Rhs {
    Atom(Atom),
    Call(Atom, Vec<Atom>),
    Con(ConId, Vec<Atom>),
    Prim(Op, Vec<Atom>, Span),
    Lambda(Lambda),
    Field(Atom, u32),
}
#[derive(Clone, Debug)]
/// Global initialization order is preserved from typed Core.
pub struct Program {
    pub globals: Vec<(Id, Body)>,
    pub metadata: core::Program,
}
#[derive(Clone)]
enum Context {
    Return,
    Jump(Id),
    Bind(Var, Body),
}
struct Convert<'a> {
    sess: &'a mut Session,
    arities: BTreeMap<Id, usize>,
}
fn arity(mut e: &erase::Expr) -> usize {
    let mut n = 0;
    while let erase::Expr::Lam(_, b) = e {
        n += 1;
        e = b;
    }
    n
}
fn collect(e: &erase::Expr, out: &mut BTreeMap<Id, usize>) {
    use erase::Expr as E;
    let mut todo = vec![e];
    while let Some(e) = todo.pop() {
        match e {
            E::Let(_, bs, b) => {
                for (v, e) in bs {
                    let n = arity(e);
                    if n > 0 {
                        out.insert(v.clone(), n);
                    }
                    todo.push(e);
                }
                todo.push(b);
            }
            E::Lam(_, b) => todo.push(b),
            E::App(a, b) | E::Join(_, _, a, b) => {
                todo.push(a);
                todo.push(b);
            }
            E::Con(_, es) | E::Prim(_, es, _) | E::Jump(_, es) => {
                todo.extend(es)
            }
            E::Case(e, _, alts) => {
                todo.push(e);
                todo.extend(alts.iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
impl Convert<'_> {
    fn fresh(&mut self) -> Var {
        self.sess.fresh_id("$a")
    }
    fn finish(&mut self, a: Atom, k: Context) -> Body {
        Rc::new(match k {
            Context::Return => Anf::Ret(a),
            Context::Jump(j) => Anf::Jump(j, vec![a]),
            Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
        })
    }
    fn rhs(&mut self, r: Rhs, k: Context) -> Body {
        if let Context::Bind(v, b) = k {
            return Rc::new(Anf::Let(v, r, b));
        }
        if let (Rhs::Call(f, args), Context::Return) = (&r, &k) {
            return Rc::new(Anf::TailCall(f.clone(), args.clone()));
        }
        let v = self.fresh();
        let rest = self.finish(Atom::Var(v.clone()), k);
        Rc::new(Anf::Let(v, r, rest))
    }
    /// Tasks hold pending compiler work. Neither expression depth nor the
    /// number of operands adds a Rust call frame.
    fn expr(&mut self, e: erase::Expr, k: Context) -> Body {
        use erase::Expr as E;
        enum Work {
            Expr(erase::Expr, Context),
            Bindings(Vec<(Var, erase::Expr)>),
            Lambda(Vec<Var>, Context),
            Rec(Vec<(Var, Vec<Var>)>),
            Case(
                erase::Expr,
                Var,
                Vec<(AltCon, Vec<Var>)>,
                Option<(Id, Var, Body)>,
            ),
            Join(Id, Vec<Var>, Option<(Id, Var, Body)>),
            Call(erase::Expr, Var, Vec<erase::Expr>, Var),
        }
        fn peel(mut e: erase::Expr) -> (Vec<Var>, erase::Expr) {
            let mut ps = vec![];
            while let E::Lam(v, b) = e {
                ps.push(v);
                e = *b;
            }
            (ps, e)
        }
        fn branch(
            k: Context,
            sess: &mut Session,
        ) -> (Context, Option<(Id, Var, Body)>) {
            match k {
                Context::Bind(v, b) => {
                    let j = sess.fresh_id("$cont");
                    (Context::Jump(j.clone()), Some((j, v, b)))
                }
                k => (k, None),
            }
        }
        fn wrap(body: Body, cont: Option<(Id, Var, Body)>) -> Body {
            match cont {
                Some((j, v, b)) => Rc::new(Anf::Join(j, vec![v], b, body)),
                None => body,
            }
        }
        let mut todo = vec![Work::Expr(e, k)];
        let mut results: Vec<Body> = vec![];
        while let Some(work) = todo.pop() {
            match work {
                Work::Bindings(mut bs) => {
                    if let Some((v, e)) = bs.pop() {
                        let b = results.pop().unwrap();
                        todo.push(Work::Bindings(bs));
                        todo.push(Work::Expr(e, Context::Bind(v, b)));
                    }
                }
                Work::Lambda(params, k) => {
                    let body = results.pop().unwrap();
                    results.push(
                        self.rhs(Rhs::Lambda(Lambda { params, body }), k),
                    );
                }
                Work::Rec(bs) => {
                    let bodies =
                        results.split_off(results.len() - bs.len());
                    let rest = results.pop().unwrap();
                    let bs = bs
                        .into_iter()
                        .zip(bodies)
                        .map(|((v, params), body)| {
                            (v, Lambda { params, body })
                        })
                        .collect();
                    results.push(Rc::new(Anf::LetRec(bs, rest)));
                }
                Work::Case(e, v, heads, cont) => {
                    let bodies =
                        results.split_off(results.len() - heads.len());
                    let alts = heads
                        .into_iter()
                        .zip(bodies)
                        .map(|((c, ps), b)| (c, ps, b))
                        .collect();
                    let b = wrap(
                        Rc::new(Anf::Case(Atom::Var(v.clone()), alts)),
                        cont,
                    );
                    todo.push(Work::Expr(e, Context::Bind(v, b)));
                }
                Work::Join(j, ps, cont) => {
                    let rest = results.pop().unwrap();
                    let body = results.pop().unwrap();
                    results.push(wrap(
                        Rc::new(Anf::Join(j, ps, body, rest)),
                        cont,
                    ));
                }
                Work::Call(head, f, args, result) => {
                    let rest = results.pop().unwrap();
                    let vars: Vec<_> =
                        args.iter().map(|_| self.fresh()).collect();
                    let atoms =
                        vars.iter().cloned().map(Atom::Var).collect();
                    let b = self.rhs(
                        Rhs::Call(Atom::Var(f.clone()), atoms),
                        Context::Bind(result, rest),
                    );
                    let bs = std::iter::once((f, head))
                        .chain(vars.into_iter().zip(args))
                        .collect();
                    results.push(b);
                    todo.push(Work::Bindings(bs));
                }
                Work::Expr(e, k) => {
                    match e {
                        E::Var(v) => {
                            results.push(self.finish(Atom::Var(v), k))
                        }
                        E::Global(v) => {
                            results.push(self.finish(Atom::Global(v), k))
                        }
                        E::Lit(l) => {
                            results.push(self.finish(Atom::Lit(l), k))
                        }
                        node @ E::Lam(..) => {
                            let (ps, body) = peel(node);
                            todo.push(Work::Lambda(ps, k));
                            todo.push(Work::Expr(body, Context::Return));
                        }
                        node @ E::App(..) => {
                            let mut head = node;
                            let mut args = vec![];

                            while let E::App(f, a) = head {
                                args.push(*a);
                                head = *f;
                            }
                            args.reverse();

                            let known = match &head {
                                E::Var(v) | E::Global(v) => {
                                    self.arities.get(v).copied()
                                }
                                E::Lam(..) => Some(arity(&head)),
                                _ => None,
                            };
                            // Unknown calls execute before evaluating their next argument.
                            let n = known.unwrap_or(1).min(args.len());
                            let extra = args.split_off(n);

                            let f = self.fresh();
                            if extra.is_empty() {
                                let vars: Vec<_> = args
                                    .iter()
                                    .map(|_| self.fresh())
                                    .collect();
                                let atoms = vars
                                    .iter()
                                    .cloned()
                                    .map(Atom::Var)
                                    .collect();
                                results.push(self.rhs(
                                    Rhs::Call(Atom::Var(f.clone()), atoms),
                                    k,
                                ));
                                todo.push(Work::Bindings(
                                    std::iter::once((f, head))
                                        .chain(vars.into_iter().zip(args))
                                        .collect(),
                                ));
                            } else {
                                let result = self.fresh();
                                let application = extra.into_iter().fold(
                                    E::Var(result.clone()),
                                    |f, a| E::App(Box::new(f), Box::new(a)),
                                );
                                todo.push(Work::Call(
                                    head, f, args, result,
                                ));
                                todo.push(Work::Expr(application, k));
                            }
                        }
                        E::Let(rec, bs, b) => {
                            if rec {
                                let (heads, bodies): (Vec<_>, Vec<_>) = bs
                                    .into_iter()
                                    .map(|(v, e)| {
                                        let (ps, e) = peel(e);
                                        ((v, ps), e)
                                    })
                                    .unzip();
                                todo.push(Work::Rec(heads));
                                todo.extend(bodies.into_iter().rev().map(
                                    |e| Work::Expr(e, Context::Return),
                                ));
                            } else {
                                todo.push(Work::Bindings(bs));
                            }
                            todo.push(Work::Expr(*b, k));
                        }
                        node @ (E::Con(..) | E::Prim(..) | E::Jump(..)) => {
                            let args = match &node {
                                E::Con(_, es)
                                | E::Prim(_, es, _)
                                | E::Jump(_, es) => es,
                                _ => unreachable!(),
                            };
                            let vars: Vec<_> =
                                args.iter().map(|_| self.fresh()).collect();
                            let atoms = vars
                                .iter()
                                .cloned()
                                .map(Atom::Var)
                                .collect();
                            let (args, body) = match node {
                                E::Con(c, es) => {
                                    (es, self.rhs(Rhs::Con(c, atoms), k))
                                }
                                E::Prim(p, es, s) => (
                                    es,
                                    self.rhs(Rhs::Prim(p, atoms, s), k),
                                ),
                                E::Jump(j, es) => {
                                    (es, Rc::new(Anf::Jump(j, atoms)))
                                }
                                _ => unreachable!(),
                            };
                            results.push(body);
                            todo.push(Work::Bindings(
                                vars.into_iter().zip(args).collect(),
                            ));
                        }
                        E::Case(e, v, alts) => {
                            let (k, cont) = branch(k, self.sess);
                            let (heads, bodies): (Vec<_>, Vec<_>) = alts
                                .into_iter()
                                .map(|(c, ps, b)| ((c, ps), b))
                                .unzip();
                            todo.push(Work::Case(*e, v, heads, cont));
                            todo.extend(
                                bodies
                                    .into_iter()
                                    .rev()
                                    .map(|e| Work::Expr(e, k.clone())),
                            );
                        }
                        E::Join(j, ps, b, r) => {
                            let (k, cont) = branch(k, self.sess);
                            todo.push(Work::Join(j, ps, cont));
                            todo.push(Work::Expr(*r, k.clone()));
                            todo.push(Work::Expr(*b, k));
                        }
                        E::Fail(s) => results.push(Rc::new(Anf::Fail(s))),
                    }
                }
            }
        }
        assert_eq!(results.len(), 1);
        results.pop().unwrap()
    }
}
/// Normalize left-to-right, sharing every non-tail continuation exactly once.
pub fn convert(sess: &mut Session, program: erase::Program) -> Program {
    let mut arities = BTreeMap::new();
    for (v, e) in &program.globals {
        if arity(e) > 0 {
            arities.insert(v.clone(), arity(e));
        }
        collect(e, &mut arities);
    }
    let mut c = Convert { sess, arities };
    let globals = program
        .globals
        .into_iter()
        .map(|(v, e)| (v, c.expr(e, Context::Return)))
        .collect();
    Program {
        globals,
        metadata: program.metadata,
    }
}
impl std::fmt::Display for Atom {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(v) => write!(f, "{v}"),
            Self::Global(v) => write!(f, "@{v}"),
            Self::Lit(l) => write!(f, "{l}"),
        }
    }
}
fn list<T: std::fmt::Display>(items: &[T]) -> String {
    items
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::Call(a, args) => write!(f, "call {a}({})", list(args)),
            Self::Con(c, args) => write!(f, "{}({})", c.name, list(args)),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::Lambda(l) => {
                write!(f, "lambda ({}) {{ {} }}", list(&l.params), l.body)
            }
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
        }
    }
}
impl std::fmt::Display for Anf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Body(&'a Anf),
            Rhs(&'a Rhs),
            Text(String),
        }
        let mut todo = vec![Work::Body(self)];
        while let Some(work) = todo.pop() {
            let mut parts = vec![];
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Rhs(Rhs::Lambda(l)) => {
                    write!(f, "lambda ({}) {{ ", list(&l.params))?;
                    parts.extend([
                        Work::Body(&l.body),
                        Work::Text(" }".into()),
                    ]);
                }
                Work::Rhs(r) => write!(f, "{r}")?,
                Work::Body(b) => match b {
                    Self::Let(v, r, b) => {
                        write!(f, "let {v} = ")?;
                        parts.extend([
                            Work::Rhs(r),
                            Work::Text(";\n".into()),
                            Work::Body(b),
                        ]);
                    }
                    Self::LetRec(bs, b) => {
                        for (v, l) in bs {
                            parts.extend([
                                Work::Text(format!(
                                    "rec {v}({}) = {{ ",
                                    list(&l.params)
                                )),
                                Work::Body(&l.body),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Body(b));
                    }
                    Self::Join(j, ps, b, r) => {
                        write!(f, "join {j}({}) = {{ ", list(ps))?;
                        parts.extend([
                            Work::Body(b),
                            Work::Text(" };\n".into()),
                            Work::Body(r),
                        ]);
                    }
                    Self::Jump(j, args) => {
                        write!(f, "jump {j}({})", list(args))?
                    }
                    Self::Case(a, alts) => {
                        writeln!(f, "case {a} {{")?;
                        for (c, ps, b) in alts {
                            let head = match c {
                                AltCon::Con(c) => c.name.clone(),
                                AltCon::Lit(l) => l.to_string(),
                                AltCon::Default => "_".into(),
                            };
                            parts.extend([
                                Work::Text(format!(
                                    "{head}({}) -> {{ ",
                                    list(ps)
                                )),
                                Work::Body(b),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Text("}".into()));
                    }
                    Self::Ret(a) => write!(f, "return {a}")?,
                    Self::TailCall(fun, args) => {
                        write!(f, "tail {fun}({})", list(args))?
                    }
                    Self::Fail(s) => write!(f, "fail {s:?}")?,
                },
            }
            todo.extend(parts.into_iter().rev());
        }
        Ok(())
    }
}
/// Drain child edges explicitly when the last owner releases a deep ANF tree.
impl Drop for Anf {
    fn drop(&mut self) {
        fn edges(node: &mut Anf, todo: &mut Vec<Body>) {
            fn take(b: &mut Body) -> Body {
                std::mem::replace(
                    b,
                    Rc::new(Anf::Ret(Atom::Lit(Lit::Int(0)))),
                )
            }
            match node {
                Anf::Let(_, r, b) => {
                    todo.push(take(b));
                    if let Rhs::Lambda(l) = r {
                        todo.push(take(&mut l.body));
                    }
                }
                Anf::LetRec(bs, b) => {
                    todo.push(take(b));
                    todo.extend(bs.drain(..).map(|(_, l)| l.body));
                }
                Anf::Join(_, _, b, r) => {
                    todo.push(take(b));
                    todo.push(take(r));
                }
                Anf::Case(_, alts) => {
                    todo.extend(alts.drain(..).map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        edges(self, &mut todo);
        while let Some(b) = todo.pop() {
            if let Ok(mut node) = Rc::try_unwrap(b) {
                edges(&mut node, &mut todo);
                // Empty this node before Drop runs again on the local variable.
                // Its remaining children are only leaf placeholders.
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Jumps transfer within a function; tail calls replace its activation.
pub enum Anf {
    Let(Var, Rhs, Body),
    LetRec(Vec<(Var, Lambda)>, Body),
    Join(Id, Vec<Var>, Body, Body),
    Jump(Id, Vec<Atom>),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Ret(Atom),
    TailCall(Atom, Vec<Atom>),
    Fail(Span),
}
}

변환기의 컨텍스트에는 세 가지 경우만 있습니다. finish는 결과가 아톰이 된 뒤에 컨텍스트를 해석합니다. 대기 중인 변환은 작업 벡터에 들어 있으므로, 긴 식이 길게 이어지는 Rust 호출 사슬이 되지 않습니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::Op,
    core::{self, AltCon, Lit},
    data::ConId,
    erase,
    names::Id,
    session::Session,
    source::Span,
};
use std::{collections::BTreeMap, rc::Rc};
/// An erased binder keeps its unique identity and diagnostic spelling.
pub type Var = Id;
/// Syntax is immutable and shared between closures and machine states.
pub type Body = Rc<Anf>;
#[derive(Clone, Debug, PartialEq, Eq)]
/// Atoms cannot perform work or fail when read from a well-scoped environment.
pub enum Atom {
    Var(Var),
    Global(Id),
    Lit(Lit),
}
#[derive(Clone, Debug)]
/// Lambda parameters are merged only across syntactically adjacent lambdas.
pub struct Lambda {
    pub params: Vec<Var>,
    pub body: Body,
}
#[derive(Clone, Debug)]
/// One strict computation whose operands have already been evaluated.
pub enum Rhs {
    Atom(Atom),
    Call(Atom, Vec<Atom>),
    Con(ConId, Vec<Atom>),
    Prim(Op, Vec<Atom>, Span),
    Lambda(Lambda),
    Field(Atom, u32),
}
#[derive(Clone, Debug)]
/// Jumps transfer within a function; tail calls replace its activation.
pub enum Anf {
    Let(Var, Rhs, Body),
    LetRec(Vec<(Var, Lambda)>, Body),
    Join(Id, Vec<Var>, Body, Body),
    Jump(Id, Vec<Atom>),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Ret(Atom),
    TailCall(Atom, Vec<Atom>),
    Fail(Span),
}
#[derive(Clone, Debug)]
/// Global initialization order is preserved from typed Core.
pub struct Program {
    pub globals: Vec<(Id, Body)>,
    pub metadata: core::Program,
}
struct Convert<'a> {
    sess: &'a mut Session,
    arities: BTreeMap<Id, usize>,
}
fn arity(mut e: &erase::Expr) -> usize {
    let mut n = 0;
    while let erase::Expr::Lam(_, b) = e {
        n += 1;
        e = b;
    }
    n
}
fn collect(e: &erase::Expr, out: &mut BTreeMap<Id, usize>) {
    use erase::Expr as E;
    let mut todo = vec![e];
    while let Some(e) = todo.pop() {
        match e {
            E::Let(_, bs, b) => {
                for (v, e) in bs {
                    let n = arity(e);
                    if n > 0 {
                        out.insert(v.clone(), n);
                    }
                    todo.push(e);
                }
                todo.push(b);
            }
            E::Lam(_, b) => todo.push(b),
            E::App(a, b) | E::Join(_, _, a, b) => {
                todo.push(a);
                todo.push(b);
            }
            E::Con(_, es) | E::Prim(_, es, _) | E::Jump(_, es) => {
                todo.extend(es)
            }
            E::Case(e, _, alts) => {
                todo.push(e);
                todo.extend(alts.iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
impl Convert<'_> {
    fn fresh(&mut self) -> Var {
        self.sess.fresh_id("$a")
    }
    fn finish(&mut self, a: Atom, k: Context) -> Body {
        Rc::new(match k {
            Context::Return => Anf::Ret(a),
            Context::Jump(j) => Anf::Jump(j, vec![a]),
            Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
        })
    }
    fn rhs(&mut self, r: Rhs, k: Context) -> Body {
        if let Context::Bind(v, b) = k {
            return Rc::new(Anf::Let(v, r, b));
        }
        if let (Rhs::Call(f, args), Context::Return) = (&r, &k) {
            return Rc::new(Anf::TailCall(f.clone(), args.clone()));
        }
        let v = self.fresh();
        let rest = self.finish(Atom::Var(v.clone()), k);
        Rc::new(Anf::Let(v, r, rest))
    }
    /// Tasks hold pending compiler work. Neither expression depth nor the
    /// number of operands adds a Rust call frame.
    fn expr(&mut self, e: erase::Expr, k: Context) -> Body {
        use erase::Expr as E;
        enum Work {
            Expr(erase::Expr, Context),
            Bindings(Vec<(Var, erase::Expr)>),
            Lambda(Vec<Var>, Context),
            Rec(Vec<(Var, Vec<Var>)>),
            Case(
                erase::Expr,
                Var,
                Vec<(AltCon, Vec<Var>)>,
                Option<(Id, Var, Body)>,
            ),
            Join(Id, Vec<Var>, Option<(Id, Var, Body)>),
            Call(erase::Expr, Var, Vec<erase::Expr>, Var),
        }
        fn peel(mut e: erase::Expr) -> (Vec<Var>, erase::Expr) {
            let mut ps = vec![];
            while let E::Lam(v, b) = e {
                ps.push(v);
                e = *b;
            }
            (ps, e)
        }
        fn branch(
            k: Context,
            sess: &mut Session,
        ) -> (Context, Option<(Id, Var, Body)>) {
            match k {
                Context::Bind(v, b) => {
                    let j = sess.fresh_id("$cont");
                    (Context::Jump(j.clone()), Some((j, v, b)))
                }
                k => (k, None),
            }
        }
        fn wrap(body: Body, cont: Option<(Id, Var, Body)>) -> Body {
            match cont {
                Some((j, v, b)) => Rc::new(Anf::Join(j, vec![v], b, body)),
                None => body,
            }
        }
        let mut todo = vec![Work::Expr(e, k)];
        let mut results: Vec<Body> = vec![];
        while let Some(work) = todo.pop() {
            match work {
                Work::Bindings(mut bs) => {
                    if let Some((v, e)) = bs.pop() {
                        let b = results.pop().unwrap();
                        todo.push(Work::Bindings(bs));
                        todo.push(Work::Expr(e, Context::Bind(v, b)));
                    }
                }
                Work::Lambda(params, k) => {
                    let body = results.pop().unwrap();
                    results.push(
                        self.rhs(Rhs::Lambda(Lambda { params, body }), k),
                    );
                }
                Work::Rec(bs) => {
                    let bodies =
                        results.split_off(results.len() - bs.len());
                    let rest = results.pop().unwrap();
                    let bs = bs
                        .into_iter()
                        .zip(bodies)
                        .map(|((v, params), body)| {
                            (v, Lambda { params, body })
                        })
                        .collect();
                    results.push(Rc::new(Anf::LetRec(bs, rest)));
                }
                Work::Case(e, v, heads, cont) => {
                    let bodies =
                        results.split_off(results.len() - heads.len());
                    let alts = heads
                        .into_iter()
                        .zip(bodies)
                        .map(|((c, ps), b)| (c, ps, b))
                        .collect();
                    let b = wrap(
                        Rc::new(Anf::Case(Atom::Var(v.clone()), alts)),
                        cont,
                    );
                    todo.push(Work::Expr(e, Context::Bind(v, b)));
                }
                Work::Join(j, ps, cont) => {
                    let rest = results.pop().unwrap();
                    let body = results.pop().unwrap();
                    results.push(wrap(
                        Rc::new(Anf::Join(j, ps, body, rest)),
                        cont,
                    ));
                }
                Work::Call(head, f, args, result) => {
                    let rest = results.pop().unwrap();
                    let vars: Vec<_> =
                        args.iter().map(|_| self.fresh()).collect();
                    let atoms =
                        vars.iter().cloned().map(Atom::Var).collect();
                    let b = self.rhs(
                        Rhs::Call(Atom::Var(f.clone()), atoms),
                        Context::Bind(result, rest),
                    );
                    let bs = std::iter::once((f, head))
                        .chain(vars.into_iter().zip(args))
                        .collect();
                    results.push(b);
                    todo.push(Work::Bindings(bs));
                }
                Work::Expr(e, k) => {
                    match e {
                        E::Var(v) => {
                            results.push(self.finish(Atom::Var(v), k))
                        }
                        E::Global(v) => {
                            results.push(self.finish(Atom::Global(v), k))
                        }
                        E::Lit(l) => {
                            results.push(self.finish(Atom::Lit(l), k))
                        }
                        node @ E::Lam(..) => {
                            let (ps, body) = peel(node);
                            todo.push(Work::Lambda(ps, k));
                            todo.push(Work::Expr(body, Context::Return));
                        }
                        node @ E::App(..) => {
                            let mut head = node;
                            let mut args = vec![];

                            while let E::App(f, a) = head {
                                args.push(*a);
                                head = *f;
                            }
                            args.reverse();

                            let known = match &head {
                                E::Var(v) | E::Global(v) => {
                                    self.arities.get(v).copied()
                                }
                                E::Lam(..) => Some(arity(&head)),
                                _ => None,
                            };
                            // Unknown calls execute before evaluating their next argument.
                            let n = known.unwrap_or(1).min(args.len());
                            let extra = args.split_off(n);

                            let f = self.fresh();
                            if extra.is_empty() {
                                let vars: Vec<_> = args
                                    .iter()
                                    .map(|_| self.fresh())
                                    .collect();
                                let atoms = vars
                                    .iter()
                                    .cloned()
                                    .map(Atom::Var)
                                    .collect();
                                results.push(self.rhs(
                                    Rhs::Call(Atom::Var(f.clone()), atoms),
                                    k,
                                ));
                                todo.push(Work::Bindings(
                                    std::iter::once((f, head))
                                        .chain(vars.into_iter().zip(args))
                                        .collect(),
                                ));
                            } else {
                                let result = self.fresh();
                                let application = extra.into_iter().fold(
                                    E::Var(result.clone()),
                                    |f, a| E::App(Box::new(f), Box::new(a)),
                                );
                                todo.push(Work::Call(
                                    head, f, args, result,
                                ));
                                todo.push(Work::Expr(application, k));
                            }
                        }
                        E::Let(rec, bs, b) => {
                            if rec {
                                let (heads, bodies): (Vec<_>, Vec<_>) = bs
                                    .into_iter()
                                    .map(|(v, e)| {
                                        let (ps, e) = peel(e);
                                        ((v, ps), e)
                                    })
                                    .unzip();
                                todo.push(Work::Rec(heads));
                                todo.extend(bodies.into_iter().rev().map(
                                    |e| Work::Expr(e, Context::Return),
                                ));
                            } else {
                                todo.push(Work::Bindings(bs));
                            }
                            todo.push(Work::Expr(*b, k));
                        }
                        node @ (E::Con(..) | E::Prim(..) | E::Jump(..)) => {
                            let args = match &node {
                                E::Con(_, es)
                                | E::Prim(_, es, _)
                                | E::Jump(_, es) => es,
                                _ => unreachable!(),
                            };
                            let vars: Vec<_> =
                                args.iter().map(|_| self.fresh()).collect();
                            let atoms = vars
                                .iter()
                                .cloned()
                                .map(Atom::Var)
                                .collect();
                            let (args, body) = match node {
                                E::Con(c, es) => {
                                    (es, self.rhs(Rhs::Con(c, atoms), k))
                                }
                                E::Prim(p, es, s) => (
                                    es,
                                    self.rhs(Rhs::Prim(p, atoms, s), k),
                                ),
                                E::Jump(j, es) => {
                                    (es, Rc::new(Anf::Jump(j, atoms)))
                                }
                                _ => unreachable!(),
                            };
                            results.push(body);
                            todo.push(Work::Bindings(
                                vars.into_iter().zip(args).collect(),
                            ));
                        }
                        E::Case(e, v, alts) => {
                            let (k, cont) = branch(k, self.sess);
                            let (heads, bodies): (Vec<_>, Vec<_>) = alts
                                .into_iter()
                                .map(|(c, ps, b)| ((c, ps), b))
                                .unzip();
                            todo.push(Work::Case(*e, v, heads, cont));
                            todo.extend(
                                bodies
                                    .into_iter()
                                    .rev()
                                    .map(|e| Work::Expr(e, k.clone())),
                            );
                        }
                        E::Join(j, ps, b, r) => {
                            let (k, cont) = branch(k, self.sess);
                            todo.push(Work::Join(j, ps, cont));
                            todo.push(Work::Expr(*r, k.clone()));
                            todo.push(Work::Expr(*b, k));
                        }
                        E::Fail(s) => results.push(Rc::new(Anf::Fail(s))),
                    }
                }
            }
        }
        assert_eq!(results.len(), 1);
        results.pop().unwrap()
    }
}
/// Normalize left-to-right, sharing every non-tail continuation exactly once.
pub fn convert(sess: &mut Session, program: erase::Program) -> Program {
    let mut arities = BTreeMap::new();
    for (v, e) in &program.globals {
        if arity(e) > 0 {
            arities.insert(v.clone(), arity(e));
        }
        collect(e, &mut arities);
    }
    let mut c = Convert { sess, arities };
    let globals = program
        .globals
        .into_iter()
        .map(|(v, e)| (v, c.expr(e, Context::Return)))
        .collect();
    Program {
        globals,
        metadata: program.metadata,
    }
}
impl std::fmt::Display for Atom {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(v) => write!(f, "{v}"),
            Self::Global(v) => write!(f, "@{v}"),
            Self::Lit(l) => write!(f, "{l}"),
        }
    }
}
fn list<T: std::fmt::Display>(items: &[T]) -> String {
    items
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::Call(a, args) => write!(f, "call {a}({})", list(args)),
            Self::Con(c, args) => write!(f, "{}({})", c.name, list(args)),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::Lambda(l) => {
                write!(f, "lambda ({}) {{ {} }}", list(&l.params), l.body)
            }
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
        }
    }
}
impl std::fmt::Display for Anf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Body(&'a Anf),
            Rhs(&'a Rhs),
            Text(String),
        }
        let mut todo = vec![Work::Body(self)];
        while let Some(work) = todo.pop() {
            let mut parts = vec![];
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Rhs(Rhs::Lambda(l)) => {
                    write!(f, "lambda ({}) {{ ", list(&l.params))?;
                    parts.extend([
                        Work::Body(&l.body),
                        Work::Text(" }".into()),
                    ]);
                }
                Work::Rhs(r) => write!(f, "{r}")?,
                Work::Body(b) => match b {
                    Self::Let(v, r, b) => {
                        write!(f, "let {v} = ")?;
                        parts.extend([
                            Work::Rhs(r),
                            Work::Text(";\n".into()),
                            Work::Body(b),
                        ]);
                    }
                    Self::LetRec(bs, b) => {
                        for (v, l) in bs {
                            parts.extend([
                                Work::Text(format!(
                                    "rec {v}({}) = {{ ",
                                    list(&l.params)
                                )),
                                Work::Body(&l.body),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Body(b));
                    }
                    Self::Join(j, ps, b, r) => {
                        write!(f, "join {j}({}) = {{ ", list(ps))?;
                        parts.extend([
                            Work::Body(b),
                            Work::Text(" };\n".into()),
                            Work::Body(r),
                        ]);
                    }
                    Self::Jump(j, args) => {
                        write!(f, "jump {j}({})", list(args))?
                    }
                    Self::Case(a, alts) => {
                        writeln!(f, "case {a} {{")?;
                        for (c, ps, b) in alts {
                            let head = match c {
                                AltCon::Con(c) => c.name.clone(),
                                AltCon::Lit(l) => l.to_string(),
                                AltCon::Default => "_".into(),
                            };
                            parts.extend([
                                Work::Text(format!(
                                    "{head}({}) -> {{ ",
                                    list(ps)
                                )),
                                Work::Body(b),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Text("}".into()));
                    }
                    Self::Ret(a) => write!(f, "return {a}")?,
                    Self::TailCall(fun, args) => {
                        write!(f, "tail {fun}({})", list(args))?
                    }
                    Self::Fail(s) => write!(f, "fail {s:?}")?,
                },
            }
            todo.extend(parts.into_iter().rev());
        }
        Ok(())
    }
}
/// Drain child edges explicitly when the last owner releases a deep ANF tree.
impl Drop for Anf {
    fn drop(&mut self) {
        fn edges(node: &mut Anf, todo: &mut Vec<Body>) {
            fn take(b: &mut Body) -> Body {
                std::mem::replace(
                    b,
                    Rc::new(Anf::Ret(Atom::Lit(Lit::Int(0)))),
                )
            }
            match node {
                Anf::Let(_, r, b) => {
                    todo.push(take(b));
                    if let Rhs::Lambda(l) = r {
                        todo.push(take(&mut l.body));
                    }
                }
                Anf::LetRec(bs, b) => {
                    todo.push(take(b));
                    todo.extend(bs.drain(..).map(|(_, l)| l.body));
                }
                Anf::Join(_, _, b, r) => {
                    todo.push(take(b));
                    todo.push(take(r));
                }
                Anf::Case(_, alts) => {
                    todo.extend(alts.drain(..).map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        edges(self, &mut todo);
        while let Some(b) = todo.pop() {
            if let Ok(mut node) = Rc::try_unwrap(b) {
                edges(&mut node, &mut todo);
                // Empty this node before Drop runs again on the local variable.
                // Its remaining children are only leaf placeholders.
            }
        }
    }
}
#[derive(Clone)]
enum Context {
    Return,
    Jump(Id),
    Bind(Var, Body),
}
fn finish(&mut self, a: Atom, k: Context) -> Body {
    Rc::new(match k {
        Context::Return => Anf::Ret(a),
        Context::Jump(j) => Anf::Jump(j, vec![a]),
        Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
    })
}
}

꼬리 위치가 아닌 case에서는 변환 과정이 나머지 컨텍스트에 새 조인 매개변수를 부여하고, 각 대안을 점프 컨텍스트로 변환합니다. 기존 Core 조인은 그대로 유지됩니다. 인접한 람다들은 하나의 다중 매개변수 람다로 합쳐집니다. 이것은 구문적 항수이며, 함수 타입에서 추론한 항수가 아닙니다.

적용 변환은 커리된 호출의 순서를 지켜야 합니다. f a b에서 호출 f a는 b가 평가되기 전에 실패할 수 있습니다. 두 인자를 하나의 즉시 평가 묶음으로 합치면 어떤 오류가 먼저 나타나는지가 바뀝니다. 변환기는 알려진 구문적 항수까지만 인자를 묶습니다. 알려지지 않은 호출은 인자를 한 번에 하나씩 소비하고, 과적용은 알려진 접두부 뒤의 경계를 보존합니다. known-prefix-error 회귀 테스트가 이 순서를 명시적으로 검사합니다.

anf_simplify는 아톰을 전파하고, 생성자가 알려진 분기를 선택하며, 사용되지 않는 전역적(total) 우변을 제거하고, 즉시 적용되는 포화(saturated) 람다를 축약합니다. 호출, 효과, 그리고 실패할 수 있는 프리미티브는 유지됩니다. 베타 규칙은 함수가 탈출하는지도 검사합니다. 물려받은 id id 예제처럼 함수를 자기 자신에게 넘기면, 그 바인딩을 그냥 삭제할 수 없습니다. 구조적 ANF 린트는 실행 전에 이름, 스코프, 조인 항수, 새 바인더를 독립적으로 검사합니다.

이 머신은 코드, 적용, 반환 상태를 도는 루프입니다. 기계의 프레임은 비꼬리 바인딩 컨티뉴에이션이나 과포화 호출 뒤에 남은 인자를 기억합니다:

#![allow(unused)]
fn main() {
use crate::{
    anf::{self, Anf, Atom, Body, Lambda, Rhs, Var},
    ast::Op,
    core::{AltCon, Lit},
    data::ConId,
    eval::Failure,
};
use std::{collections::BTreeMap, rc::Rc};
type Env = crate::env::Env<u32, Value>;
type Joins = BTreeMap<u32, Rc<Join>>;
#[derive(Clone, Debug)]
/// Strict data, lexical closures, and partial applications.
pub enum Value {
    Lit(Lit),
    Con(ConId, Rc<Vec<Value>>, Option<Vec<String>>),
    Closure(Rc<Closure>),
    Pap(Rc<Value>, Vec<Value>),
}
impl Drop for Value {
    fn drop(&mut self) {
        fn children(value: &mut Value, todo: &mut Vec<Value>) {
            match value {
                Value::Con(_, fields, _) => {
                    if !fields.is_empty()
                        && let Ok(values) = Rc::try_unwrap(
                            std::mem::replace(fields, Rc::new(vec![])),
                        )
                    {
                        todo.extend(values);
                    }
                }
                Value::Pap(base, args) => {
                    todo.append(args);
                    if let Some(value) = Rc::get_mut(base) {
                        todo.push(std::mem::replace(
                            value,
                            Value::Lit(Lit::Int(0)),
                        ));
                    }
                }
                Value::Closure(c) => {
                    if let Some(c) = Rc::get_mut(c)
                        && let Some(env) = Rc::get_mut(&mut c.env)
                    {
                        env.drain_unique(todo);
                    }
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        children(self, &mut todo);
        while let Some(mut value) = todo.pop() {
            children(&mut value, &mut todo);
        }
    }
}
#[derive(Clone, Debug)]
/// Recursive groups are shared by index, avoiding reference-count cycles.
pub struct Closure {
    lambda: Lambda,
    env: Rc<Env>,
    recursive: Option<usize>,
}
#[derive(Clone)]
struct Join {
    params: Vec<Var>,
    body: Body,
    env: Env,
    joins: Joins,
}
enum Frame {
    Bind(Var, Body, Env, Joins),
    Apply(Vec<Value>),
}
#[derive(Clone, Copy, Debug, Default)]
/// Continuation depth counts pending non-tail calls, not recursive tail transfers.
pub struct Stats {
    pub steps: usize,
    pub max_continuations: usize,
}
struct Machine<'a> {
    groups: Vec<Env>,
    program: &'a anf::Program,
    globals: Env,
    env: Env,
    joins: Joins,
    stack: Vec<Frame>,
    fuel: usize,
    stats: Stats,
}
fn key(v: &Var) -> u32 {
    v.name.unique
}
fn closure(
    lambda: Lambda,
    env: Rc<Env>,
    recursive: Option<usize>,
) -> Value {
    Value::Closure(Rc::new(Closure {
        lambda,
        env,
        recursive,
    }))
}
fn function(body: &Body) -> Option<Lambda> {
    if let Anf::Let(v, Rhs::Lambda(l), b) = body.as_ref()
        && matches!(b.as_ref(),Anf::Ret(Atom::Var(w)) if v==w)
    {
        Some(l.clone())
    } else {
        None
    }
}
impl Machine<'_> {
    fn atom(&self, a: &Atom) -> Result<Value, Failure> {
        match a {
            Atom::Lit(l) => Ok(Value::Lit(l.clone())),
            Atom::Var(v) => {
                self.env.get(&key(v)).cloned().ok_or_else(|| {
                    format!("internal error: unbound ANF variable {v}")
                        .into()
                })
            }
            Atom::Global(v) => {
                self.globals.get(&key(v)).cloned().ok_or_else(|| {
                    format!("internal error: uninitialized ANF global {v}")
                        .into()
                })
            }
        }
    }
    fn atoms(&self, args: &[Atom]) -> Result<Vec<Value>, Failure> {
        args.iter().map(|a| self.atom(a)).collect()
    }
    fn rhs(&self, r: &Rhs) -> Result<Value, Failure> {
        match r {
            Rhs::Atom(a) => self.atom(a),
            Rhs::Con(c, args) => Ok(Value::Con(
                c.clone(),
                Rc::new(self.atoms(args)?),
                self.program.metadata.constructors[c.index as usize]
                    .labels
                    .clone(),
            )),
            Rhs::Lambda(l) => {
                Ok(closure(l.clone(), Rc::new(self.env.clone()), None))
            }
            Rhs::Field(a, i) => match &self.atom(a)? {
                Value::Con(_, fields, _) => {
                    fields.get(*i as usize).cloned().ok_or_else(|| {
                        "internal error: invalid field index".into()
                    })
                }
                _ => Err("internal error: field on non-constructor".into()),
            },
            Rhs::Prim(op, args, _) => {
                let p = match op {
                    Op::Runtime(p) => *p,
                    Op::Add => crate::primitives::Prim::IntAdd,
                    Op::Sub => crate::primitives::Prim::IntSub,
                    Op::Mul => crate::primitives::Prim::IntMul,
                    Op::Div => crate::primitives::Prim::IntDiv,
                    Op::Mod => crate::primitives::Prim::IntMod,
                    Op::Eq => crate::primitives::Prim::IntEq,
                    Op::Lt => crate::primitives::Prim::IntLt,
                    Op::Le => crate::primitives::Prim::IntLe,
                    Op::Append => crate::primitives::Prim::StrAppend,
                };
                let values = self.atoms(args)?;
                let args = values
                    .iter()
                    .map(to_core)
                    .collect::<Result<Vec<_>, _>>()?;
                crate::primitives::evaluate(
                    p,
                    &args,
                    &self.program.metadata,
                )
                .map(from_core)
            }
            Rhs::Call(..) => unreachable!("calls are machine transitions"),
        }
    }
    fn run(&mut self, mut state: State) -> Result<Value, Failure> {
        loop {
            if self.fuel == 0 {
                return Err("step limit exceeded".into());
            }
            self.fuel -= 1;
            self.stats.steps += 1;
            self.stats.max_continuations =
                self.stats.max_continuations.max(self.stack.len());
            state = match state {
                State::Code(body) => match body.as_ref() {
                    Anf::Ret(a) => State::Return(self.atom(a)?),
                    Anf::TailCall(f, args) => {
                        State::Apply(self.atom(f)?, self.atoms(args)?)
                    }
                    Anf::Let(v, Rhs::Call(f, args), rest) => {
                        let f = self.atom(f)?;
                        let args = self.atoms(args)?;
                        self.stack.push(Frame::Bind(
                            v.clone(),
                            rest.clone(),
                            std::mem::take(&mut self.env),
                            std::mem::take(&mut self.joins),
                        ));
                        State::Apply(f, args)
                    }
                    Anf::Let(v, r, rest) => {
                        let value = self.rhs(r)?;
                        self.env.insert(key(v), value);
                        State::Code(rest.clone())
                    }
                    Anf::LetRec(bindings, rest) => {
                        let index = self.groups.len();
                        let env = Rc::new(self.env.clone());
                        for (v, l) in bindings {
                            self.env.insert(
                                key(v),
                                closure(
                                    l.clone(),
                                    env.clone(),
                                    Some(index),
                                ),
                            );
                        }
                        self.groups.push(self.env.clone());
                        State::Code(rest.clone())
                    }
                    Anf::Join(j, params, body, rest) => {
                        let join = Join {
                            params: params.clone(),
                            body: body.clone(),
                            env: self.env.clone(),
                            joins: self.joins.clone(),
                        };
                        self.joins.insert(key(j), Rc::new(join));
                        State::Code(rest.clone())
                    }
                    Anf::Jump(j, args) => {
                        let values = self.atoms(args)?;
                        let join =
                            self.joins.get(&key(j)).cloned().ok_or(
                                "internal error: unbound ANF join",
                            )?;
                        if values.len() != join.params.len() {
                            return Err(
                                "internal error: ANF jump arity".into()
                            );
                        }
                        self.env = join.env.clone();
                        self.joins = join.joins.clone();
                        self.env.extend(
                            join.params.iter().map(key).zip(values),
                        );
                        State::Code(join.body.clone())
                    }
                    Anf::Case(a, alts) => {
                        let value = self.atom(a)?;
                        let mut chosen = None;
                        for (c, ps, b) in alts {
                            let fields = match (c, &value) {
                                (AltCon::Default, _) => Some(vec![]),
                                (
                                    AltCon::Con(c),
                                    Value::Con(d, fields, _),
                                ) if c == d => Some((**fields).clone()),
                                (AltCon::Lit(a), Value::Lit(b))
                                    if a == b =>
                                {
                                    Some(vec![])
                                }
                                _ => None,
                            };
                            if let Some(fields) = fields {
                                self.env
                                    .extend(ps.iter().map(key).zip(fields));
                                chosen = Some(b.clone());
                                break;
                            }
                        }
                        State::Code(chosen.ok_or(
                            "internal error: ANF case has no alternative",
                        )?)
                    }
                    Anf::Fail(span) => {
                        let site = self
                            .program
                            .metadata
                            .locations
                            .iter()
                            .find(|(s, _)| s == span)
                            .map(|(_, s)| s.as_str())
                            .unwrap_or("<unknown>:1:1");
                        return Err(format!(
                            "pattern match failure at {site}"
                        )
                        .into());
                    }
                },
                State::Apply(f, args) => match &f {
                    Value::Pap(base, earlier) => {
                        let mut earlier = earlier.clone();
                        earlier.extend(args);
                        State::Apply((**base).clone(), earlier)
                    }
                    Value::Closure(c) => {
                        let arity = c.lambda.params.len();
                        if args.len() < arity {
                            State::Return(Value::Pap(
                                Rc::new(Value::Closure(c.clone())),
                                args,
                            ))
                        } else {
                            let mut args = args;
                            let extra = args.split_off(arity);
                            if !extra.is_empty() {
                                self.stack.push(Frame::Apply(extra));
                            }
                            self.env = (*c.env).clone();
                            self.joins.clear();
                            if let Some(index) = c.recursive {
                                self.env = self.groups[index].clone();
                            }
                            self.env.extend(
                                c.lambda.params.iter().map(key).zip(args),
                            );
                            State::Code(c.lambda.body.clone())
                        }
                    }
                    _ => {
                        return Err(
                            "internal error: application of non-function"
                                .into(),
                        );
                    }
                },
                State::Return(value) => match self.stack.pop() {
                    None => return Ok(value),
                    Some(Frame::Apply(args)) => State::Apply(value, args),
                    Some(Frame::Bind(v, b, mut env, joins)) => {
                        env.insert(key(&v), value);
                        self.env = env;
                        self.joins = joins;
                        State::Code(b)
                    }
                },
            };
        }
    }
}
/// Independent control flow, with shared monomorphic primitive definitions.
pub fn run(
    program: &anf::Program,
    fuel: usize,
    io: bool,
) -> Result<(Value, Stats), Failure> {
    let mut m = Machine {
        groups: vec![],
        program,
        globals: Env::new(),
        env: Env::new(),
        joins: Joins::new(),
        stack: vec![],
        fuel,
        stats: Stats::default(),
    };
    for (v, b) in &program.globals {
        if let Some(l) = function(b) {
            m.globals
                .insert(key(v), closure(l, Rc::new(Env::new()), None));
        }
    }
    for (v, b) in &program.globals {
        if function(b).is_none() {
            m.env.clear();
            m.joins.clear();
            let value = m.run(State::Code(b.clone()))?;
            m.globals.insert(key(v), value);
        }
    }
    let main = m
        .globals
        .get(&program.metadata.main.unique())
        .cloned()
        .ok_or("internal error: missing main")?;
    let value = if io {
        let unit = program
            .metadata
            .constructors
            .iter()
            .find(|c| c.id.name == "()")
            .ok_or("internal error: missing unit")?;
        m.run(State::Apply(
            main,
            vec![Value::Con(unit.id.clone(), Rc::new(vec![]), None)],
        ))?
    } else {
        main
    };
    Ok((value, m.stats))
}
fn to_core(v: &Value) -> Result<crate::eval::Value, Failure> {
    Ok(match v {
        Value::Lit(l) => crate::eval::Value::Lit(l.clone()),
        Value::Con(c, fields, labels) => crate::eval::Value::Con(
            c.clone(),
            Rc::new(fields.iter().map(to_core).collect::<Result<_, _>>()?),
            labels.clone(),
        ),
        _ => {
            return Err(
                "internal error: function passed to a data primitive"
                    .into(),
            );
        }
    })
}
fn from_core(v: crate::eval::Value) -> Value {
    match &v {
        crate::eval::Value::Lit(l) => Value::Lit(l.clone()),
        crate::eval::Value::Con(c, fields, labels) => Value::Con(
            c.clone(),
            Rc::new(fields.iter().cloned().map(from_core).collect()),
            labels.clone(),
        ),
        _ => unreachable!("primitive cannot return a closure"),
    }
}
impl std::fmt::Display for Value {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.print(f, 0)
    }
}
impl Value {
    fn print(
        &self,
        f: &mut std::fmt::Formatter<'_>,
        precedence: u8,
    ) -> std::fmt::Result {
        match self {
            Self::Lit(Lit::Double(n))
                if f64::from_bits(*n).is_sign_negative()
                    && precedence > 6 =>
            {
                write!(
                    f,
                    "({})",
                    crate::primitives::show_double(f64::from_bits(*n))
                )
            }
            Self::Lit(Lit::Int(n)) if *n < 0 && precedence > 6 => {
                write!(f, "({n})")
            }
            Self::Lit(l) => write!(f, "{l}"),
            Self::Closure(_) | Self::Pap(..) => write!(f, "<function>"),
            Self::Con(id, fields, Some(labels)) => {
                if precedence > 10 {
                    write!(f, "(")?;
                }
                write!(f, "{} {{", id.name)?;
                for (i, (name, value)) in
                    labels.iter().zip(fields.iter()).enumerate()
                {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{name} = ")?;
                    value.print(f, 0)?;
                }
                write!(f, "}}")?;
                if precedence > 10 {
                    write!(f, ")")?;
                }
                Ok(())
            }
            Self::Con(id, fields, None) => match id.name.as_str() {
                "[]" => write!(f, "[]"),
                ":" => {
                    write!(f, "[")?;
                    let mut value = self;
                    let mut first = true;
                    while let Self::Con(c, fs, _) = value {
                        if c.name == "[]" {
                            break;
                        }
                        if !first {
                            write!(f, ",")?;
                        }
                        first = false;
                        fs[0].print(f, 0)?;
                        value = &fs[1];
                    }
                    write!(f, "]")
                }
                "()" | "(,)" | "(,,)" => {
                    write!(f, "(")?;
                    for (i, v) in fields.iter().enumerate() {
                        if i > 0 {
                            write!(f, ",")?;
                        }
                        v.print(f, 0)?;
                    }
                    write!(f, ")")
                }
                _ => {
                    let parens = precedence > 10 && !fields.is_empty();
                    if parens {
                        write!(f, "(")?;
                    }
                    write!(f, "{}", id.name)?;
                    for v in fields.iter() {
                        write!(f, " ")?;
                        v.print(f, 11)?;
                    }
                    if parens {
                        write!(f, ")")?;
                    }
                    Ok(())
                }
            },
        }
    }
}
enum State {
    Code(Body),
    Apply(Value, Vec<Value>),
    Return(Value),
}
use crate::{
    anf::{self, Anf, Atom, Body, Lambda, Rhs, Var},
    ast::Op,
    core::{AltCon, Lit},
    data::ConId,
    eval::Failure,
};
use std::{collections::BTreeMap, rc::Rc};
type Env = crate::env::Env<u32, Value>;
type Joins = BTreeMap<u32, Rc<Join>>;
#[derive(Clone, Debug)]
/// Strict data, lexical closures, and partial applications.
pub enum Value {
    Lit(Lit),
    Con(ConId, Rc<Vec<Value>>, Option<Vec<String>>),
    Closure(Rc<Closure>),
    Pap(Rc<Value>, Vec<Value>),
}
impl Drop for Value {
    fn drop(&mut self) {
        fn children(value: &mut Value, todo: &mut Vec<Value>) {
            match value {
                Value::Con(_, fields, _) => {
                    if !fields.is_empty()
                        && let Ok(values) = Rc::try_unwrap(
                            std::mem::replace(fields, Rc::new(vec![])),
                        )
                    {
                        todo.extend(values);
                    }
                }
                Value::Pap(base, args) => {
                    todo.append(args);
                    if let Some(value) = Rc::get_mut(base) {
                        todo.push(std::mem::replace(
                            value,
                            Value::Lit(Lit::Int(0)),
                        ));
                    }
                }
                Value::Closure(c) => {
                    if let Some(c) = Rc::get_mut(c)
                        && let Some(env) = Rc::get_mut(&mut c.env)
                    {
                        env.drain_unique(todo);
                    }
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        children(self, &mut todo);
        while let Some(mut value) = todo.pop() {
            children(&mut value, &mut todo);
        }
    }
}
#[derive(Clone, Debug)]
/// Recursive groups are shared by index, avoiding reference-count cycles.
pub struct Closure {
    lambda: Lambda,
    env: Rc<Env>,
    recursive: Option<usize>,
}
#[derive(Clone)]
struct Join {
    params: Vec<Var>,
    body: Body,
    env: Env,
    joins: Joins,
}
enum State {
    Code(Body),
    Apply(Value, Vec<Value>),
    Return(Value),
}
#[derive(Clone, Copy, Debug, Default)]
/// Continuation depth counts pending non-tail calls, not recursive tail transfers.
pub struct Stats {
    pub steps: usize,
    pub max_continuations: usize,
}
struct Machine<'a> {
    groups: Vec<Env>,
    program: &'a anf::Program,
    globals: Env,
    env: Env,
    joins: Joins,
    stack: Vec<Frame>,
    fuel: usize,
    stats: Stats,
}
fn key(v: &Var) -> u32 {
    v.name.unique
}
fn closure(
    lambda: Lambda,
    env: Rc<Env>,
    recursive: Option<usize>,
) -> Value {
    Value::Closure(Rc::new(Closure {
        lambda,
        env,
        recursive,
    }))
}
fn function(body: &Body) -> Option<Lambda> {
    if let Anf::Let(v, Rhs::Lambda(l), b) = body.as_ref()
        && matches!(b.as_ref(),Anf::Ret(Atom::Var(w)) if v==w)
    {
        Some(l.clone())
    } else {
        None
    }
}
impl Machine<'_> {
    fn atom(&self, a: &Atom) -> Result<Value, Failure> {
        match a {
            Atom::Lit(l) => Ok(Value::Lit(l.clone())),
            Atom::Var(v) => {
                self.env.get(&key(v)).cloned().ok_or_else(|| {
                    format!("internal error: unbound ANF variable {v}")
                        .into()
                })
            }
            Atom::Global(v) => {
                self.globals.get(&key(v)).cloned().ok_or_else(|| {
                    format!("internal error: uninitialized ANF global {v}")
                        .into()
                })
            }
        }
    }
    fn atoms(&self, args: &[Atom]) -> Result<Vec<Value>, Failure> {
        args.iter().map(|a| self.atom(a)).collect()
    }
    fn rhs(&self, r: &Rhs) -> Result<Value, Failure> {
        match r {
            Rhs::Atom(a) => self.atom(a),
            Rhs::Con(c, args) => Ok(Value::Con(
                c.clone(),
                Rc::new(self.atoms(args)?),
                self.program.metadata.constructors[c.index as usize]
                    .labels
                    .clone(),
            )),
            Rhs::Lambda(l) => {
                Ok(closure(l.clone(), Rc::new(self.env.clone()), None))
            }
            Rhs::Field(a, i) => match &self.atom(a)? {
                Value::Con(_, fields, _) => {
                    fields.get(*i as usize).cloned().ok_or_else(|| {
                        "internal error: invalid field index".into()
                    })
                }
                _ => Err("internal error: field on non-constructor".into()),
            },
            Rhs::Prim(op, args, _) => {
                let p = match op {
                    Op::Runtime(p) => *p,
                    Op::Add => crate::primitives::Prim::IntAdd,
                    Op::Sub => crate::primitives::Prim::IntSub,
                    Op::Mul => crate::primitives::Prim::IntMul,
                    Op::Div => crate::primitives::Prim::IntDiv,
                    Op::Mod => crate::primitives::Prim::IntMod,
                    Op::Eq => crate::primitives::Prim::IntEq,
                    Op::Lt => crate::primitives::Prim::IntLt,
                    Op::Le => crate::primitives::Prim::IntLe,
                    Op::Append => crate::primitives::Prim::StrAppend,
                };
                let values = self.atoms(args)?;
                let args = values
                    .iter()
                    .map(to_core)
                    .collect::<Result<Vec<_>, _>>()?;
                crate::primitives::evaluate(
                    p,
                    &args,
                    &self.program.metadata,
                )
                .map(from_core)
            }
            Rhs::Call(..) => unreachable!("calls are machine transitions"),
        }
    }
    fn run(&mut self, mut state: State) -> Result<Value, Failure> {
        loop {
            if self.fuel == 0 {
                return Err("step limit exceeded".into());
            }
            self.fuel -= 1;
            self.stats.steps += 1;
            self.stats.max_continuations =
                self.stats.max_continuations.max(self.stack.len());
            state = match state {
                State::Code(body) => match body.as_ref() {
                    Anf::Ret(a) => State::Return(self.atom(a)?),
                    Anf::TailCall(f, args) => {
                        State::Apply(self.atom(f)?, self.atoms(args)?)
                    }
                    Anf::Let(v, Rhs::Call(f, args), rest) => {
                        let f = self.atom(f)?;
                        let args = self.atoms(args)?;
                        self.stack.push(Frame::Bind(
                            v.clone(),
                            rest.clone(),
                            std::mem::take(&mut self.env),
                            std::mem::take(&mut self.joins),
                        ));
                        State::Apply(f, args)
                    }
                    Anf::Let(v, r, rest) => {
                        let value = self.rhs(r)?;
                        self.env.insert(key(v), value);
                        State::Code(rest.clone())
                    }
                    Anf::LetRec(bindings, rest) => {
                        let index = self.groups.len();
                        let env = Rc::new(self.env.clone());
                        for (v, l) in bindings {
                            self.env.insert(
                                key(v),
                                closure(
                                    l.clone(),
                                    env.clone(),
                                    Some(index),
                                ),
                            );
                        }
                        self.groups.push(self.env.clone());
                        State::Code(rest.clone())
                    }
                    Anf::Join(j, params, body, rest) => {
                        let join = Join {
                            params: params.clone(),
                            body: body.clone(),
                            env: self.env.clone(),
                            joins: self.joins.clone(),
                        };
                        self.joins.insert(key(j), Rc::new(join));
                        State::Code(rest.clone())
                    }
                    Anf::Jump(j, args) => {
                        let values = self.atoms(args)?;
                        let join =
                            self.joins.get(&key(j)).cloned().ok_or(
                                "internal error: unbound ANF join",
                            )?;
                        if values.len() != join.params.len() {
                            return Err(
                                "internal error: ANF jump arity".into()
                            );
                        }
                        self.env = join.env.clone();
                        self.joins = join.joins.clone();
                        self.env.extend(
                            join.params.iter().map(key).zip(values),
                        );
                        State::Code(join.body.clone())
                    }
                    Anf::Case(a, alts) => {
                        let value = self.atom(a)?;
                        let mut chosen = None;
                        for (c, ps, b) in alts {
                            let fields = match (c, &value) {
                                (AltCon::Default, _) => Some(vec![]),
                                (
                                    AltCon::Con(c),
                                    Value::Con(d, fields, _),
                                ) if c == d => Some((**fields).clone()),
                                (AltCon::Lit(a), Value::Lit(b))
                                    if a == b =>
                                {
                                    Some(vec![])
                                }
                                _ => None,
                            };
                            if let Some(fields) = fields {
                                self.env
                                    .extend(ps.iter().map(key).zip(fields));
                                chosen = Some(b.clone());
                                break;
                            }
                        }
                        State::Code(chosen.ok_or(
                            "internal error: ANF case has no alternative",
                        )?)
                    }
                    Anf::Fail(span) => {
                        let site = self
                            .program
                            .metadata
                            .locations
                            .iter()
                            .find(|(s, _)| s == span)
                            .map(|(_, s)| s.as_str())
                            .unwrap_or("<unknown>:1:1");
                        return Err(format!(
                            "pattern match failure at {site}"
                        )
                        .into());
                    }
                },
                State::Apply(f, args) => match &f {
                    Value::Pap(base, earlier) => {
                        let mut earlier = earlier.clone();
                        earlier.extend(args);
                        State::Apply((**base).clone(), earlier)
                    }
                    Value::Closure(c) => {
                        let arity = c.lambda.params.len();
                        if args.len() < arity {
                            State::Return(Value::Pap(
                                Rc::new(Value::Closure(c.clone())),
                                args,
                            ))
                        } else {
                            let mut args = args;
                            let extra = args.split_off(arity);
                            if !extra.is_empty() {
                                self.stack.push(Frame::Apply(extra));
                            }
                            self.env = (*c.env).clone();
                            self.joins.clear();
                            if let Some(index) = c.recursive {
                                self.env = self.groups[index].clone();
                            }
                            self.env.extend(
                                c.lambda.params.iter().map(key).zip(args),
                            );
                            State::Code(c.lambda.body.clone())
                        }
                    }
                    _ => {
                        return Err(
                            "internal error: application of non-function"
                                .into(),
                        );
                    }
                },
                State::Return(value) => match self.stack.pop() {
                    None => return Ok(value),
                    Some(Frame::Apply(args)) => State::Apply(value, args),
                    Some(Frame::Bind(v, b, mut env, joins)) => {
                        env.insert(key(&v), value);
                        self.env = env;
                        self.joins = joins;
                        State::Code(b)
                    }
                },
            };
        }
    }
}
/// Independent control flow, with shared monomorphic primitive definitions.
pub fn run(
    program: &anf::Program,
    fuel: usize,
    io: bool,
) -> Result<(Value, Stats), Failure> {
    let mut m = Machine {
        groups: vec![],
        program,
        globals: Env::new(),
        env: Env::new(),
        joins: Joins::new(),
        stack: vec![],
        fuel,
        stats: Stats::default(),
    };
    for (v, b) in &program.globals {
        if let Some(l) = function(b) {
            m.globals
                .insert(key(v), closure(l, Rc::new(Env::new()), None));
        }
    }
    for (v, b) in &program.globals {
        if function(b).is_none() {
            m.env.clear();
            m.joins.clear();
            let value = m.run(State::Code(b.clone()))?;
            m.globals.insert(key(v), value);
        }
    }
    let main = m
        .globals
        .get(&program.metadata.main.unique())
        .cloned()
        .ok_or("internal error: missing main")?;
    let value = if io {
        let unit = program
            .metadata
            .constructors
            .iter()
            .find(|c| c.id.name == "()")
            .ok_or("internal error: missing unit")?;
        m.run(State::Apply(
            main,
            vec![Value::Con(unit.id.clone(), Rc::new(vec![]), None)],
        ))?
    } else {
        main
    };
    Ok((value, m.stats))
}
fn to_core(v: &Value) -> Result<crate::eval::Value, Failure> {
    Ok(match v {
        Value::Lit(l) => crate::eval::Value::Lit(l.clone()),
        Value::Con(c, fields, labels) => crate::eval::Value::Con(
            c.clone(),
            Rc::new(fields.iter().map(to_core).collect::<Result<_, _>>()?),
            labels.clone(),
        ),
        _ => {
            return Err(
                "internal error: function passed to a data primitive"
                    .into(),
            );
        }
    })
}
fn from_core(v: crate::eval::Value) -> Value {
    match &v {
        crate::eval::Value::Lit(l) => Value::Lit(l.clone()),
        crate::eval::Value::Con(c, fields, labels) => Value::Con(
            c.clone(),
            Rc::new(fields.iter().cloned().map(from_core).collect()),
            labels.clone(),
        ),
        _ => unreachable!("primitive cannot return a closure"),
    }
}
impl std::fmt::Display for Value {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.print(f, 0)
    }
}
impl Value {
    fn print(
        &self,
        f: &mut std::fmt::Formatter<'_>,
        precedence: u8,
    ) -> std::fmt::Result {
        match self {
            Self::Lit(Lit::Double(n))
                if f64::from_bits(*n).is_sign_negative()
                    && precedence > 6 =>
            {
                write!(
                    f,
                    "({})",
                    crate::primitives::show_double(f64::from_bits(*n))
                )
            }
            Self::Lit(Lit::Int(n)) if *n < 0 && precedence > 6 => {
                write!(f, "({n})")
            }
            Self::Lit(l) => write!(f, "{l}"),
            Self::Closure(_) | Self::Pap(..) => write!(f, "<function>"),
            Self::Con(id, fields, Some(labels)) => {
                if precedence > 10 {
                    write!(f, "(")?;
                }
                write!(f, "{} {{", id.name)?;
                for (i, (name, value)) in
                    labels.iter().zip(fields.iter()).enumerate()
                {
                    if i > 0 {
                        write!(f, ", ")?;
                    }
                    write!(f, "{name} = ")?;
                    value.print(f, 0)?;
                }
                write!(f, "}}")?;
                if precedence > 10 {
                    write!(f, ")")?;
                }
                Ok(())
            }
            Self::Con(id, fields, None) => match id.name.as_str() {
                "[]" => write!(f, "[]"),
                ":" => {
                    write!(f, "[")?;
                    let mut value = self;
                    let mut first = true;
                    while let Self::Con(c, fs, _) = value {
                        if c.name == "[]" {
                            break;
                        }
                        if !first {
                            write!(f, ",")?;
                        }
                        first = false;
                        fs[0].print(f, 0)?;
                        value = &fs[1];
                    }
                    write!(f, "]")
                }
                "()" | "(,)" | "(,,)" => {
                    write!(f, "(")?;
                    for (i, v) in fields.iter().enumerate() {
                        if i > 0 {
                            write!(f, ",")?;
                        }
                        v.print(f, 0)?;
                    }
                    write!(f, ")")
                }
                _ => {
                    let parens = precedence > 10 && !fields.is_empty();
                    if parens {
                        write!(f, "(")?;
                    }
                    write!(f, "{}", id.name)?;
                    for v in fields.iter() {
                        write!(f, " ")?;
                        v.print(f, 11)?;
                    }
                    if parens {
                        write!(f, ")")?;
                    }
                    Ok(())
                }
            },
        }
    }
}
enum Frame {
    Bind(Var, Body, Env, Joins),
    Apply(Vec<Value>),
}
}

클로저는 람다 하나와 공유되는 렉시컬 환경을 담습니다. 재귀 그룹은 머신이 소유한 테이블에 한 번만 만들어집니다. 그 클로저들은 그룹의 인덱스를 보유하므로, 참조 카운트 순환이 생기지 않고 호출할 때마다 클로저를 다시 만들 필요도 없습니다. 인자를 너무 적게 적용하면 부분 적용이 반환됩니다. 남는 인자는 반환된 함수에 적용됩니다. 꼬리 호출은 현재 상태를 대체하고, 점프는 렉시컬하게 캡처된 조인 환경으로 진입합니다. 두 전이 모두 Rust 인터프리터를 재귀적으로 호출하지 않습니다. 원시 값은 데이터만 다루는 어댑터를 거쳐 기존 원시 연산 구현으로 넘어갑니다. 제어 흐름은 Core 평가기를 호출하지 않습니다.

실행해 보기

$ cd crates/ch18_anf
$ cargo run -q -- run tests/programs/anf/case-continuation.fun --lint
7
$ cargo run -q -- dump --pass=types tests/programs/anf/case-continuation.fun
main : IO ()

타입이 지워진 Core를 보려면 --pass=erase를, 이름 붙은 연산을 보려면 --pass=anf를 사용하십시오. 변환과 재작성을 구분하려면 후자를 --no-simplify와 함께 다시 실행하십시오. 기준 결과와 비교하려면 --engine=core로 실행을 반복하십시오. 덤프에는 Prelude가 포함됩니다. tests/anf.rs의 작은 스냅숏들은 꼬리 위치가 아닌 case나 가드 실패 조인 같은 개별 변환을 따로 떼어 보여 줍니다.

손으로 작성한 예제와 함께, testkit/src/gen.rs는 산술, 지역 바인딩, 분기, 재귀, 리스트를 포함하는 작고 타입이 올바른 프로그램을 생성합니다. 두 엔진은 모두 독립적인 래핑 정수 계산 결과와 일치해야 합니다. 우리는 실패한 경우의 소스와 시드를 저장하여, 예상치 못한 답이 디버깅할 수 있는 프로그램이 되도록 합니다.

백만 번 호출하는 합계 계산은 머신의 제어 흐름을 시험하기에 유용합니다. 우리는 단지 끝나는지만 확인하는 대신, 대기 중인 컨티뉴에이션의 수를 측정합니다. 꼬리 호출은 현재 계산을 대체해야 합니다. 비꼬리 호출은 힙에 할당된 컨티뉴에이션 스택에 대기 중인 작업을 추가할 수 있습니다. 어느 쪽도 Rust 호출 프레임을 추가로 필요로 하지 않습니다. Core는 자체적인 명시적 기계를 사용하므로, 이를 ANF와 비교하면 변환이 답을 바꾸지 않았는지도 확인할 수 있습니다.

연습 문제

  1. 준비 운동. 단순화하지 않은 ANF 덤프에서 예제의 case 뒤에 오는 컨티뉴에이션을 찾으십시오.
  2. 중급. 두 인자가 서로 다른 메시지로 실패하도록 만들고, 알려진 호출과 알려지지 않은 호출에서 각각 어느 쪽이 먼저 실행되어야 하는지 확인하십시오.
  3. 중급. 정수 나눗셈에는 IO 효과가 없는데도 사용되지 않는 나눗셈을 제거하는 것이 왜 잘못일 수 있는지 설명하십시오.
  4. 고급. 꼬리 재귀 합계와 꼬리 재귀가 아닌 합계에 대해 머신에 계측 코드를 넣고 최대 컨티뉴에이션 깊이를 비교하십시오.
  5. 고급. 결정적 생성기에 타입이 올바른 새 구문 하나와 그에 대한 독립적인 기대 결과 계산을 추가하여 확장하십시오.

더 읽을거리

19장: 클로저 변환

새 파일

  • src/closure.rs
  • src/flat.rs
  • src/flat_machine.rs
  • src/flat_lint.rs

변경된 파일

  • src/lib.rs
  • src/main.rs

동기

함수는 자신을 만든 호출보다 오래 살아남을 수 있습니다. Fun에서는 이런 코드를 쉽게 작성할 수 있지만, 이 때문에 우리 백엔드에는 한 가지 질문이 남습니다. 그 지난 호출의 변수들은 이제 어디에 존재합니까?

우리 인터프리터는 각 함수와 함께 환경을 저장하는 방식으로 이 질문에 답했습니다. 우리는 같은 아이디어를 컴파일하는 프로그램 안에서 명시적으로 드러낼 것입니다. 각 람다는 프로그램 전체에 걸친 테이블의 함수가 됩니다. 그 클로저는 해당 함수가 바깥 스코프에서 필요로 하는 값들을 담고 다닙니다.

또한 이 함수들을 어떻게 호출할지도 알아볼 것입니다. 호출 대상을 알고 있을 때는 직접 호출할 수 있습니다. 함수가 인자로 전달되거나 다른 호출에서 반환되어 올 때는 그 클로저를 검사해야 합니다. 부분 적용은 흥미로운 경우를 하나 더 추가합니다. 네이티브 메모리에 객체를 배치하기 시작하기 전에, 우리는 이 모든 것을 Flat 인터프리터로 모델링할 것입니다.

현재의 언어

다음은 x를 기억하며 map에 전달할 수 있는 작은 함수입니다.

make x = \y -> x+y

main = print (map (make 20) [1,2,3])

반환된 함수를 호출할 때마다 전달된 값 20이 필요합니다. 여기서는 ANF 언커링(uncurrying)이 인접한 람다들을 하나의 전역 함수로 합치므로, 그 부분 적용은 해당 값을 PAP에 보관합니다. 오버로드된 덧셈에는 Num 사전도 필요합니다. 반면 바인딩이나 분기로 분리된 지역 람다는 명시적으로 캡처된 환경을 필요로 합니다. 부분 적용, 과포화 적용, 재귀 지역 함수, 그리고 값으로 사용되는 클래스 사전이 모두 계속 동작해야 합니다. 이제 --engine=flat이 기본값입니다. Core와 ANF도 여전히 선택할 수 있습니다.

클로저 환경은 컴파일러가 만드는 저장 공간입니다. 소스 언어에는 가변 필드나 참조 연산이 추가되지 않습니다. 여기서 새로 도입되는 유일한 변경 연산은 사용자 코드가 클로저를 관찰할 수 있기 전에 재귀 그룹의 캡처 슬롯을 초기화하는 것입니다.

이론

자유 변수 분석은 람다가 자신의 매개변수 외에 필요로 하는 값을 찾아냅니다. 전역 값은 프로그램이 직접 주소를 지정할 수 있으므로 지역 캡처 슬롯이 필요하지 않습니다. 남은 각 자유 변수에 대해 변환은 안정적인 슬롯을 선택하고, 클로저를 생성할 때 값을 저장하며, 끌어올려진 함수 안에서 렉시컬 접근을 환경 로드로 대체합니다.

클로저 변환과 람다 리프팅은 서로 관련이 있지만 다른 선택지입니다. 클로저 변환은 캡처한 값을 하나의 환경으로 묶습니다. 람다 리프팅은 그 대신 호출 지점이 허용하는 경우 그 값을 명시적인 함수 매개변수로 추가할 수 있습니다 (Appel 1992). 이 장에서는 지역 함수에 대해, 탈출하지 않는 함수까지 포함하여 하나의 환경 메커니즘만 유지합니다. 선택적 람다 리프팅은 뒤로 미룹니다. 알려진 지역 호출은 여전히 환경을 명시적으로 넘기면서 일반 apply를 우회할 수 있습니다.

소스 함수는 조금씩 나누어 호출될 수 있습니다. 우리가 끌어올린 기계 함수는 정해진 개수의 인자를 기대합니다. 이 두 관점을 어떻게 조화시킬 수 있겠습니까?

함수의 구문적 항수가 n인데 인자가 그보다 적으면, 우리는 인자를 부분 적용, 즉 PAP에 저장합니다. 정확히 n개를 가지고 있다면 함수에 진입합니다. 더 많이 가지고 있다면 n개로 함수를 호출한 다음, 남은 인자들을 그 함수가 반환한 함수에 적용합니다. 이것이 축소판 eval/apply 프로토콜입니다 (Marlow and Peyton Jones 2004). 이제 하나의 호출 메커니즘이 세 경우를 모두 처리합니다.

상호 재귀는 닭이 먼저냐 달걀이 먼저냐의 문제를 일으킵니다. even이 odd를 캡처하고 odd가 even을 캡처한다면, 어느 클로저를 먼저 만들 수 있겠습니까? 우리는 두 클로저를 자리 표시자 캡처 슬롯으로 할당한 다음, 두 주소가 모두 생기면 슬롯을 채울 것입니다. 이 초기화가 끝나기 전에는 어떤 소스 코드도 이 클로저들을 호출할 수 없습니다. 재귀 바인딩이 함수여야 한다고 요구하기 때문에, 완성되지 않은 재귀 값을 평가하려 하지 않고도 이것이 가능해집니다.

구현

Flat 문법

Flat은 ANF의 명시적인 제어 흐름을 유지하지만, 이제 함수는 별도의 함수 테이블에 존재합니다. 실행 가능한 본문 안에는 람다 형식이 없습니다. 대신 끌어올려진 함수를 가리키고 그 캡처를 저장하는 클로저를 할당합니다.

아톰 , join 이름 , 대안 태그 , 그리고 윗줄로 표시한 시퀀스에는 ANF의 표기법을 그대로 사용합니다. 이름 는 끌어올려진 함수를 식별합니다. 선택적인 환경 피연산자 는 아톰이거나 없습니다. 알려진 호출(known call)은 자신의 코드를 직접 지명합니다. 알려지지 않은 호출(unknown call)은 무엇을 할지 알아내기 위해 클로저나 PAP를 검사해야 합니다.

여기서 는 생성자, 는 원시 연산, 는 슬롯 인덱스입니다. 프리미티브의 소스 스팬은 생략했습니다. patch는 재귀 클로저를 서로 엮는 동안 쓰이는 내부 연산이며, Fun 프로그램이 사용할 수 있는 가변 상태가 아닙니다. ANF의 LetRec은 할당과 그 뒤를 잇는 이러한 패치들로 바뀌어 사라졌습니다.

끌어올려진 함수와 호출

Flat IR은 실행 가능한 본문에서 람다 표현식을 제거합니다. 각 끌어올려진 함수는 매개변수, 선택적인 환경 매개변수, 본문, 그리고 순서가 정해진 캡처 목록을 기록합니다.

#![allow(unused)]
fn main() {
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Let(v, r, b) => write!(f, "let {v} = {r};\n{b}"),
            Self::Join(j, ps, b, r) => {
                write!(f, "join {j}({}) = {{ {b} }};\n{r}", list(ps))
            }
            Self::Jump(j, args) => write!(f, "jump {j}({})", list(args)),
            Self::Ret(a) => write!(f, "return {a}"),
            Self::TailCallKnown(id, env, args) => write!(
                f,
                "tail {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::TailCallUnknown(a, args) => {
                write!(f, "tail-apply {a}({})", list(args))
            }
            Self::Fail(s) => write!(f, "fail {s:?}"),
            Self::Case(a, alts) => {
                writeln!(f, "case {a} {{")?;
                for (c, ps, b) in alts {
                    let head = match c {
                        AltCon::Con(c) => c.name.clone(),
                        AltCon::Lit(l) => l.to_string(),
                        AltCon::Default => "_".into(),
                    };
                    writeln!(f, "{head}({}) -> {{ {b} }}", list(ps))?;
                }
                write!(f, "}}")
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
}

closure::convert는 람다 정의를 수집하고, 자유 변수를 계산하며, 사용처를 분석합니다. 캡처 순서는 고유 이름 순서를 따르므로 덤프와 슬롯 인덱스가 결정적입니다. 최상위 함수 항목에는 환경 매개변수가 없습니다. 리프팅된 지역 항목에는 환경이 비어 있더라도 환경 매개변수가 있습니다. 각 지역 함수의 시작 부분에 있는 접두 EnvField 바인딩이 캡처를 불러옵니다.

호출은 인자 개수가 구문적 항수와 일치하고, 그 머리가 전역 함수이거나 탈출하지 않는 지역 람다일 때에만 알려진 호출입니다. 단지 함수 타입을 가졌다는 것만으로는 충분하지 않습니다. 실제 결정 과정은 직접 살펴볼 수 있을 만큼 작습니다.

#![allow(unused)]
fn main() {
fn known(&self, a: &Atom, n: usize) -> Option<(FuncId, Option<Atom>)> {
    let (Atom::Var(v) | Atom::Global(v)) = a else {
        return None;
    };
    let info = self.infos.get(v)?;
    (n == info.lambda.params.len() && (info.global || !info.escapes))
        .then(|| (info.id, (!info.global).then(|| a.clone())))
}
}

탈출 분석은 피호출자로서의 포화된 사용을 함수를 반환하거나, 저장하거나, 인자로 전달하는 것과 다르게 취급합니다. 알려진 지역 호출도 여전히 자신의 클로저를 환경 피연산자로 전달합니다. 그 밖의 호출은 CallUnknown을 사용합니다. 꼬리 위치에서는 그에 대응하는 꼬리 호출 형태가 선택됩니다. 따라서 부분 적용은 소스 표기상 익숙한 함수를 지명하더라도 계속 apply를 거칩니다.

우변 언어는 할당과 접근을 눈에 보이게 드러냅니다.

#![allow(unused)]
fn main() {
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Let(v, r, b) => write!(f, "let {v} = {r};\n{b}"),
            Self::Join(j, ps, b, r) => {
                write!(f, "join {j}({}) = {{ {b} }};\n{r}", list(ps))
            }
            Self::Jump(j, args) => write!(f, "jump {j}({})", list(args)),
            Self::Ret(a) => write!(f, "return {a}"),
            Self::TailCallKnown(id, env, args) => write!(
                f,
                "tail {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::TailCallUnknown(a, args) => {
                write!(f, "tail-apply {a}({})", list(args))
            }
            Self::Fail(s) => write!(f, "fail {s:?}"),
            Self::Case(a, alts) => {
                writeln!(f, "case {a} {{")?;
                for (c, ps, b) in alts {
                    let head = match c {
                        AltCon::Con(c) => c.name.clone(),
                        AltCon::Lit(l) => l.to_string(),
                        AltCon::Default => "_".into(),
                    };
                    writeln!(f, "{head}({}) -> {{ {b} }}", list(ps))?;
                }
                write!(f, "}}")
            }
        }
    }
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
}

LetRec에 대해 변환은 모든 AllocClosure를 어떤 PatchClosure보다도 먼저 내보냅니다. IR에서 초기 캡처 슬롯에는 자리 표시자 정수 리터럴이 들어 있습니다. 패치한 뒤에는 각 슬롯에 실제로 캡처한 값이 들어갑니다. 패치 연산은 사용되지 않는 임시 바인더에 unit을 반환합니다. 모든 패치가 변환된 본문보다 앞서므로, 어떤 소스 표현식도 부분적으로만 초기화된 클로저를 호출할 수 없습니다.

Flat 린트는 각 끌어올려진 함수를 독립적으로 검사합니다. 린트는 암묵적인 자유 변수, 알 수 없는 함수 정체, 잘못된 알려진 호출 항수, 누락되었거나 예상치 못한 환경 피연산자, 범위를 벗어난 환경 로드, 잘못된 join 전달을 거부합니다. 각 환경 로드는 별도의 함수 스코프에서 이름을 다시 바인딩하므로, 캡처 바인더 식별자는 서로 다른 함수에서 반복해서 나타날 수 있습니다.

Flat 머신은 클로저 레코드를 담는 인덱스 기반 아레나를 소유합니다. 클로저 값은 아레나 인덱스입니다. 환경 슬롯에는 다른 클로저 인덱스를 포함한 일반적인 인터프리터 값이 들어 있습니다. 덕분에 참조 카운트 순환 없이 재귀적인 순환을 표현할 수 있습니다. PAP는 함수 값과 지금까지 공급된 인자를 저장합니다. 아레나는 한 번의 실행 동안 존재하며 객체 간의 관계를 모델링합니다. 메모리 회수는 아직 모델링하지 않습니다.

실행은 코드, 기지 호출, 적용(apply), 반환 상태를 따로 두는 반복 루프입니다. 알려진 호출은 진입점의 항수와 환경을 검증합니다. 적용은 PAP 인자를 평탄화하고, 인자가 부족한 호출과 정확한 호출을 처리하며, 남는 인자는 명시적인 컨티뉴에이션에 보관합니다. join은 함수 안에서 일어나는 어휘적 제어 전이로 남습니다. 기본 연산의 피연산자는 데이터 어댑터를 거치지만, 평가 과정에서 Flat 본문을 Core나 ANF 인터프리터에 위임하는 일은 결코 없습니다. 세 엔진이 제어 흐름을 각자 독립적으로 구현하기 때문에 결과가 일치한다는 사실에 의미가 있습니다.

실행해 보기

$ cd crates/ch19_closure
$ cargo run -q -- run tests/programs/closure/capture-return.fun --lint
[21,22,23]
$ cargo run -q -- dump --pass=types tests/programs/closure/capture-return.fun
main : IO ()

이 파일을 --pass=flat으로 덤프하여 make의 부분 적용을 찾아보십시오. 전역 진입점에는 환경이 없습니다. 적용은 사전(dictionary)과 정수 인자를 PAP에 저장합니다. 반면 escape-recursive.fun은 바깥 값을 캡처하는 지역 재귀 클로저를 반환하고, known-local.fun은 직접적인 지역 호출을 사용합니다. pap-chain.fun은 인자를 여러 단계에 걸쳐 공급합니다. over-application.fun은 결과 자체가 함수인 함수에 인자를 공급합니다. tests/anf.rs의 작은 Flat 스냅숏은 도달 가능한 코드만 골라내어 관련 없는 Prelude 함수를 생략합니다.

인자를 여러 단계에 걸쳐 공급해 보고, 지역 함수를 반환해 보고, 두 지역 함수가 모두 바깥 값을 캡처한 채로 서로를 호출하게 만들어 보십시오. 이 연습 문제들은 모두 소스에서는 자연스러워 보이지만, 각각 클로저 메커니즘의 서로 다른 부분을 시험합니다. Core, ANF, Flat의 결과는 일치해야 합니다. Flat 머신은 긴 꼬리 재귀 합계 계산이 대기 중인 컨티뉴에이션의 수를 일정한 한도 안으로 유지하는지도 검사합니다.

연습 문제

  1. 준비 운동. 언커링(uncurrying)과 부분 적용이 예제의 정수 인자와 Num 사전을 어떻게 보존하는지 추적하십시오.
  2. 중급. 포화 호출로만 쓰이던 지역 함수를 값으로 반환하도록 바꾸십시오. 그 결과 호출이 어떻게 분류되는지 설명하십시오.
  3. 중급. PAP가 인자를 하나 더 받은 뒤, 첫 번째 결과가 클로저인 과포화 호출이 이어지는 과정을 추적하십시오.
  4. 고급. 바깥의 정수도 캡처하는 상호 재귀 지역 함수를 구성하십시오. 패칭 후의 환경 그래프를 그리십시오.
  5. 고급. 탈출하지 않는 함수에 대한 람다 리프팅의 프로토타입을 만들고, 차등 일치를 유지하면서 할당 횟수를 비교하십시오.

더 읽을거리

20장: 데이터 표현과 런타임

새 파일

  • rt/src/layout.rs
  • rt/src/abi.rs
  • rt/src/heap.rs
  • rt/src/apply.rs
  • rt/src/primitives.rs
  • rt/build.rs
  • rt/src/lib.rs

변경된 파일

  • src/primitives.rs
  • src/lib.rs
  • src/main.rs

동기

지금까지 우리 인터프리터는 쓸모 있는 호사를 누려 왔습니다. Rust 열거형이 값을 기술하고, Rust가 그 저장 공간 관리를 돕습니다. 이제 우리가 생성할 기계어 코드에는 그런 열거형이 없습니다. 정수, 리스트 노드, 클로저가 메모리에서 정확히 어떤 모습인지 우리가 정해야 합니다.

다형성 때문에 이 질문은 특히 흥미로워집니다. 컴파일된 id 함수 하나가 정수, 문자열, 트리를 모두 받을 수 있어야 합니다. 우리는 이 모든 값을 같은 호출 규약으로 전달하고 싶습니다. 균일한 기계어 워드를 쓰면 그런 공통 표현을 얻을 수 있고, 약간의 태깅으로 즉칫값과 포인터를 구별할 수 있습니다.

LLVM을 연결하기 전에 런타임부터 만들겠습니다. 이렇게 하면 메모리 레이아웃과 적용 프로토콜을 따로 떼어 살펴볼 수 있습니다. 인터프리터들도 런타임의 기본 연산을 재사용할 것입니다. 부동소수점 수를 출력하는 방법에 대해 조금씩 다른 생각을 세 가지나 둘 필요는 없을 것입니다.

현재의 언어

여기서 우리가 작성하는 프로그램은 바뀌지 않습니다. 새 표현도 여전히 래핑되는 63비트 정수, 유니코드 문자, UTF-8 문자열, 엄격한 생성자 필드를 제공해야 합니다. 값이 메모리에 들어가는 방식을 바꾸더라도 Fun 프로그램이 관찰할 수 있는 것이 바뀌어서는 안 됩니다.

이 상속된 프로그램으로 NaN, 무한대, 부호 있는 0의 부동소수점 표시를 연습 문제처럼 시험해 봅니다:

_old = (0.0/0.0,1.0/0.0,-0.0)

main = print _old

런타임은 fun-rt-20으로 제공되며, 컴파일러를 위한 rlib와 외부 링크를 위한 staticlib 두 가지 형태로 모두 빌드됩니다. 이 장의 컴파일러는 여전히 Core, ANF, Flat을 실행합니다. 런타임을 C 테스트에 링크한다고 해서 아직 fun build가 생기는 것은 아닙니다. LLVM 오브젝트 생성은 다음 장에서 할 일입니다.

이론

값은 64비트 워드입니다. 즉치값(immediate)은 최하위 비트가 1입니다. 정렬된 포인터는 최하위 비트가 0입니다. 정수를 인코딩할 때는 왼쪽으로 1비트 시프트한 뒤 태그를 설정하므로, 부호 있는 63비트가 남습니다. 디코딩에는 산술 시프트를 사용합니다. 같은 즉칫값 인코딩으로 문자 스칼라 값과 인자 없는 생성자 태그도 담습니다.

#![allow(unused)]
fn main() {
pub type Word = u64;
pub const WORD_BYTES: usize = 8;
pub const INT_MIN: i64 = -(1_i64 << 62);
pub const INT_MAX: i64 = (1_i64 << 62) - 1;
pub const CON: u8 = 0;
pub const CLOSURE: u8 = 1;
pub const PAP: u8 = 2;
pub const DOUBLE: u8 = 3;
pub const STRING: u8 = 4;
pub const FORWARD: u8 = 5;
pub const FALSE: Word = immediate(0);
pub const TRUE: Word = immediate(1);
pub const NIL: Word = immediate(0);
pub const UNIT: Word = immediate(0);
pub const NOTHING: Word = immediate(0);
pub const LT: Word = immediate(0);
pub const EQ: Word = immediate(1);
pub const GT: Word = immediate(2);
pub const CONS_TAG: u32 = 1;
pub const JUST_TAG: u32 = 1;
pub const CLOSURE_CODE: usize = 0;
pub const CLOSURE_ARITY: usize = 1;
pub const CLOSURE_CAPTURES: usize = 2;
pub const PAP_CLOSURE: usize = 0;
pub const PAP_COUNT: usize = 1;
pub const PAP_ARGS: usize = 2;
pub const fn normalize(n: i64) -> i64 {
    (n << 1) >> 1
}
pub const fn signed(w: Word) -> i64 {
    (w as i64) >> 1
}
pub const fn immediate(tag: u32) -> Word {
    ((tag as Word) << 1) | 1
}
pub const fn is_immediate(w: Word) -> bool {
    w & 1 != 0
}
/// Bits 0..8: kind, 8..32: tag, 32..64: payload word count.
pub const fn header(kind: u8, tag: u32, size: u32) -> Word {
    assert!(tag <= 0x00ff_ffff);
    kind as Word | ((tag as Word) << 8) | ((size as Word) << 32)
}
pub const fn kind(h: Word) -> u8 {
    h as u8
}
pub const fn tag(h: Word) -> u32 {
    ((h >> 8) & 0x00ff_ffff) as u32
}
pub const fn size(h: Word) -> usize {
    (h >> 32) as usize
}
pub const fn integer(n: i64) -> Word {
    ((n as u64) << 1) | 1
}
pub type Word = u64;
pub const WORD_BYTES: usize = 8;
pub const INT_MIN: i64 = -(1_i64 << 62);
pub const INT_MAX: i64 = (1_i64 << 62) - 1;
pub const CON: u8 = 0;
pub const CLOSURE: u8 = 1;
pub const PAP: u8 = 2;
pub const DOUBLE: u8 = 3;
pub const STRING: u8 = 4;
pub const FORWARD: u8 = 5;
pub const FALSE: Word = immediate(0);
pub const TRUE: Word = immediate(1);
pub const NIL: Word = immediate(0);
pub const UNIT: Word = immediate(0);
pub const NOTHING: Word = immediate(0);
pub const LT: Word = immediate(0);
pub const EQ: Word = immediate(1);
pub const GT: Word = immediate(2);
pub const CONS_TAG: u32 = 1;
pub const JUST_TAG: u32 = 1;
pub const CLOSURE_CODE: usize = 0;
pub const CLOSURE_ARITY: usize = 1;
pub const CLOSURE_CAPTURES: usize = 2;
pub const PAP_CLOSURE: usize = 0;
pub const PAP_COUNT: usize = 1;
pub const PAP_ARGS: usize = 2;
pub const fn normalize(n: i64) -> i64 {
    (n << 1) >> 1
}
pub const fn integer(n: i64) -> Word {
    ((n as u64) << 1) | 1
}
pub const fn immediate(tag: u32) -> Word {
    ((tag as Word) << 1) | 1
}
pub const fn is_immediate(w: Word) -> bool {
    w & 1 != 0
}
/// Bits 0..8: kind, 8..32: tag, 32..64: payload word count.
pub const fn header(kind: u8, tag: u32, size: u32) -> Word {
    assert!(tag <= 0x00ff_ffff);
    kind as Word | ((tag as Word) << 8) | ((size as Word) << 32)
}
pub const fn kind(h: Word) -> u8 {
    h as u8
}
pub const fn tag(h: Word) -> u32 {
    ((h >> 8) & 0x00ff_ffff) as u32
}
pub const fn size(h: Word) -> usize {
    (h >> 32) as usize
}
pub const fn signed(w: Word) -> i64 {
    (w as i64) >> 1
}
}

워드 1에 관해 이상한 점이 하나 있습니다. 이 워드는 유닛, False, 빈 리스트, Nothing 중 무엇이든 나타낼 수 있습니다. 이는 런타임이 해소해야 하는 모호함이 아닙니다. 프로그램이 어떤 해석을 사용할 수 있는지는 타입 검사기가 이미 확정했습니다. 우리는 리스트를 불리언인 것처럼 검사하지 않습니다.

바로 이 점이 균일한 표현을 유용하게 만듭니다. 다형 함수는 인스턴스화마다 다른 메모리 레이아웃을 필요로 하지 않고도 워드를 주고받을 수 있습니다. 태깅과 박싱은 엄격한 함수형 언어 구현이 이를 실현할 수 있는 실용적인 방법을 제공합니다(Leroy 1990).

힙 객체는 헤더로 시작합니다. 헤더의 하위 8비트는 객체 종류를 식별하고, 그다음 24비트는 생성자 태그를 담으며, 상위 32비트는 페이로드 워드 수를 셉니다. 생성자 태그는 데이터 타입마다 따로 매겨지며 선언 순서를 따릅니다. Prelude의 고정 태그 덕분에 런타임이 만든 리스트와 옵션 값이 컴파일러가 만든 것과 호환됩니다.

Boehm은 메모리의 워드를 조사하여 포인터일 가능성이 있는 값을 찾습니다 (Boehm and Weiser 1988). 덕분에 우리는 생성된 코드에서 살아 있는 포인터를 하나하나 기술하지 않아도 됩니다. 트레이드오프는 무관한 비트 패턴이 주소처럼 보여서, 원래는 죽은 객체를 살아 있게 붙잡아 둘 수 있다는 점입니다. 또한 수집기는 겉보기 포인터 중 어느 것이 갱신해야 할 실제 참조인지 모르기 때문에 객체를 자유롭게 옮길 수 없습니다.

정밀한 이동 수집기에는 우리의 도움이 필요합니다. 우리는 24장에서 루트를 명시적으로 관리하는 방식으로 그 협력을 구축할 것입니다. Henderson과 GC 핸드북에서 이 구분을 자세히 다룹니다 [Henderson (2002); Jones et al. (2023)].

구현

rt/src/layout.rs는 공유 ABI 정의이며, 컴파일러는 기존의 오프사이드 레이아웃 모듈과 겹치지 않도록 이를 runtime_layout이라는 이름으로 다시 내보냅니다. 헤더를 만들 때는 생성자 태그가 들어맞는지 검사합니다:

#![allow(unused)]
fn main() {
pub type Word = u64;
pub const WORD_BYTES: usize = 8;
pub const INT_MIN: i64 = -(1_i64 << 62);
pub const INT_MAX: i64 = (1_i64 << 62) - 1;
pub const CON: u8 = 0;
pub const CLOSURE: u8 = 1;
pub const PAP: u8 = 2;
pub const DOUBLE: u8 = 3;
pub const STRING: u8 = 4;
pub const FORWARD: u8 = 5;
pub const FALSE: Word = immediate(0);
pub const TRUE: Word = immediate(1);
pub const NIL: Word = immediate(0);
pub const UNIT: Word = immediate(0);
pub const NOTHING: Word = immediate(0);
pub const LT: Word = immediate(0);
pub const EQ: Word = immediate(1);
pub const GT: Word = immediate(2);
pub const CONS_TAG: u32 = 1;
pub const JUST_TAG: u32 = 1;
pub const CLOSURE_CODE: usize = 0;
pub const CLOSURE_ARITY: usize = 1;
pub const CLOSURE_CAPTURES: usize = 2;
pub const PAP_CLOSURE: usize = 0;
pub const PAP_COUNT: usize = 1;
pub const PAP_ARGS: usize = 2;
pub const fn normalize(n: i64) -> i64 {
    (n << 1) >> 1
}
pub const fn integer(n: i64) -> Word {
    ((n as u64) << 1) | 1
}
pub const fn signed(w: Word) -> i64 {
    (w as i64) >> 1
}
pub const fn immediate(tag: u32) -> Word {
    ((tag as Word) << 1) | 1
}
pub const fn is_immediate(w: Word) -> bool {
    w & 1 != 0
}
pub const fn kind(h: Word) -> u8 {
    h as u8
}
pub const fn tag(h: Word) -> u32 {
    ((h >> 8) & 0x00ff_ffff) as u32
}
pub const fn size(h: Word) -> usize {
    (h >> 32) as usize
}
/// Bits 0..8: kind, 8..32: tag, 32..64: payload word count.
pub const fn header(kind: u8, tag: u32, size: u32) -> Word {
    assert!(tag <= 0x00ff_ffff);
    kind as Word | ((tag as Word) << 8) | ((size as Word) << 32)
}
}

페이로드 레이아웃은 일부러 단순하게 만들었습니다:

종류헤더 뒤의 페이로드
CON생성자 필드, 필드마다 한 워드
CLOSURE느린 코드 주소, 항수, 그다음 캡처
PAP클로저 워드, 인자 개수, 그다음 전달된 인자
DOUBLEbinary64 비트를 담은 한 워드
STRING바이트 길이, 그다음 패딩된 UTF-8 바이트
FORWARD나중에 만들 복사 컬렉터를 위해 예약됨

힙 읽기

변수에 담긴 워드에서 시작합시다. 이 워드의 최하위 비트를 보면 값이 그 워드 안에 들어가는지, 아니면 객체를 가리키는지 알 수 있습니다. 포인터는 첫 번째 필드가 아니라 헤더를 가리킵니다. 아래의 오프셋은 모두 바이트 단위이며, 각 행은 8바이트를 차지합니다.

Value word (64 bits)

Int 42:     [ 85 ]  low bit 1
Heap value: [ p  ]  low bit 0
              |
              v
+----------------------------+
| header                     | +0
| payload[0]                 | +8
| payload[1]                 | +16
| ...                        |
+----------------------------+

Header bits (most significant first)
 63             32 31           8 7            0
+-----------------+--------------+--------------+
| size: 32 bits   | tag: 24 bits | kind: 8 bits |
+-----------------+--------------+--------------+

예를 들어 Just 42에는 페이로드 워드가 하나 필요합니다. 이 값의 생성자 태그는 1입니다. Double도 페이로드 워드가 하나 필요하지만, 그 워드에는 인코딩된 Fun 값이 아니라 부동소수점 비트가 들어 있습니다.

Just 42
+----------------------------+
| CON, tag=1, size=1         | +0
| 85 (tagged Int 42)         | +8
+----------------------------+

Double 3.0
+----------------------------+
| DOUBLE, tag=0, size=1      | +0
| 0x4008000000000000         | +8
+----------------------------+

클로저는 코드와 그 코드가 캡처한 값들을 하나로 묶습니다. k = 10일 때 지역 함수 f x y = k + x + y를 생각해 봅시다. 이 함수의 항수는 2이고 환경에는 캡처가 하나 있습니다. f를 20 하나에만 적용하면 부분 적용(partial application), 즉 PAP가 만들어집니다. PAP는 원래 클로저를 가리키며 그 인자를 기억합니다.

PAP for f 20
+----------------------------+
| PAP, tag=0, size=3         | +0
| closure word               | +8 ----+
| argument count: 1          | +16    |
| 41 (tagged Int 20)         | +24    |
+----------------------------+        |
       +------------------------------+
       |
       |  Closure for f
       |  +----------------------------+
       +->| CLOSURE, tag=0, size=3     | +0
          | slow entry address         | +8
          | arity: 2                   | +16
          | capture k: 21 (Int 10)     | +24
          +----------------------------+

화살표는 클로저 객체 전체를 가리키는 포인터를 나타냅니다. 항수와 인자 개수는 태그가 붙은 정수가 아니라 원시 개수입니다. 캡처와 저장된 인자는 Fun 값 워드이므로, 그 자체가 다른 객체를 가리킬 수 있습니다. 느린 진입점(slow entry)은 코드 주소입니다. PAP를 22로 호출하면 저장된 20과 22를 합친 다음, 두 인자를 모두 가지고 클로저에 진입합니다. 클로저의 캡처는 여전히 10입니다.

문자열은 바이트 수를 저장하고, 그 뒤에 워드 단위로 패딩된 UTF-8 바이트를 저장합니다. "hi"의 경우 페이로드는 두 워드, 즉 바이트 수 한 워드와 바이트 저장용 한 워드로 이루어집니다. 패딩은 문자열의 일부가 아니며, 끝을 나타내는 0도 필요하지 않습니다.

String "hi"
+----------------------------+
| STRING, tag=0, size=2      | +0
| byte length: 2             | +8
| 68 69 .. .. .. .. .. ..    | +16
+----------------------------+
Hex bytes; .. denotes padding.

rt_alloc은 헤더를 포함한 전체 워드 수를 받습니다. rt_alloc_atomic은 바이트 수를 받고, Boehm에게 내용을 스캔하지 말라고 요청합니다. 배정밀도 실수(double)와 문자열은 페이로드에 값 포인터가 없으므로 원자적(atomic) 할당을 사용합니다. 문자열에 저장된 길이는 바이트 수를 셉니다. 소스 언어의 문자열 길이 프리미티브는 유니코드 스칼라 값의 개수를 셉니다. 정렬은 전체적으로 8바이트입니다.

할당은 수집기가 실행되는 동안 입력 필드를 보호해야 합니다. Rust Vec은 Rust의 할당자로 할당되며, Boehm은 그 버퍼를 자동으로 추적하지 않습니다. Roots는 위치가 고정된 박스 워드 버퍼를 소유하고, 그 내부 범위를 수집기에 등록합니다. 소멸자는 버퍼를 해제하기 전에 그 범위를 등록 해제합니다. 등록되지 않은 가드 워드 두 개가 인접한 등록 범위들을 서로 구별되게 유지합니다. 객체 생성은 필드를 할당하고 복사하기 전에 이 보호 장치를 사용합니다:

#![allow(unused)]
fn main() {
use crate::layout::{self, Word};
use std::{ffi::c_void, ptr, sync::Once};
#[repr(C)]
struct StackBase {
    mem_base: *mut c_void,
}
unsafe extern "C" {
    fn GC_init();
    fn GC_allow_register_threads();
    fn GC_thread_is_registered() -> i32;
    fn GC_get_stack_base(base: *mut StackBase) -> i32;
    fn GC_register_my_thread(base: *const StackBase) -> i32;
    fn GC_unregister_my_thread() -> i32;
    fn GC_malloc(n: usize) -> *mut c_void;
    fn GC_malloc_atomic(n: usize) -> *mut c_void;
    fn GC_add_roots(start: *mut c_void, end: *mut c_void);
    fn GC_remove_roots(start: *mut c_void, end: *mut c_void);
    fn GC_gcollect();
}
static INIT: Once = Once::new();
struct Registration(bool);
impl Drop for Registration {
    fn drop(&mut self) {
        if self.0 {
            // SAFETY: only this thread's successful explicit registration is removed.
            unsafe {
                GC_unregister_my_thread();
            }
        }
    }
}
thread_local! {
    static THREAD: Registration = {
        // SAFETY: INIT completed first; StackBase matches gc.h on our supported targets.
        unsafe {
            if GC_thread_is_registered() != 0 { Registration(false) }
            else {
                let mut base = StackBase { mem_base: ptr::null_mut() };
                assert_eq!(GC_get_stack_base(&mut base), 0, "cannot find GC stack base");
                assert_eq!(GC_register_my_thread(&base), 0, "cannot register GC thread");
                Registration(true)
            }
        }
    };
}
/// Call on the main thread before starting runtime worker threads.
#[unsafe(no_mangle)]
pub extern "C" fn rt_init() {
    crate::primitives::init_io();
    INIT.call_once(|| {
        // SAFETY: initialization is serialized and precedes every heap operation.
        unsafe {
            GC_init();
            GC_allow_register_threads();
        }
    });
    THREAD.with(|_| {});
}
fn checked(p: *mut c_void) -> *mut c_void {
    if p.is_null() {
        eprintln!("fun: out of memory");
        std::process::abort();
    }
    assert_eq!(p as usize % layout::WORD_BYTES, 0);
    p
}
/// Allocate total words, including the header, with pointer scanning enabled.
#[unsafe(no_mangle)]
pub extern "C" fn rt_alloc(words: usize) -> *mut Word {
    rt_init();
    let bytes = words
        .checked_mul(layout::WORD_BYTES)
        .expect("allocation size overflow");
    // SAFETY: the initialized collector returns suitably aligned storage of this size.
    checked(unsafe { GC_malloc(bytes.max(8)) }).cast()
}
/// Allocate bytes for a pointer-free object; callers initialize its header and payload.
#[unsafe(no_mangle)]
pub extern "C" fn rt_alloc_atomic(bytes: usize) -> *mut u8 {
    rt_init();
    // SAFETY: the initialized collector returns aligned storage, never scanned as pointers.
    checked(unsafe { GC_malloc_atomic(bytes.max(8)) }).cast()
}
/// A registered, independently owned range. Rust's ordinary heap is not scanned by Boehm.
pub struct Roots {
    words: Box<[Word]>,
}
impl Roots {
    pub fn new(words: &[Word]) -> Self {
        // Unregistered guard words prevent Boehm merging adjacent root ranges.
        let mut storage = vec![0; words.len() + 2];
        storage[1..words.len() + 1].copy_from_slice(words);
        let words = storage.into_boxed_slice();
        rt_init();
        if words.len() > 2 {
            // SAFETY: this stable, exclusively owned allocation lasts until Drop removes it.
            unsafe {
                GC_add_roots(
                    words.as_ptr().add(1) as *mut c_void,
                    words.as_ptr().add(words.len() - 1) as *mut c_void,
                );
            }
        }
        Self { words }
    }
    pub fn as_slice(&self) -> &[Word] {
        &self.words[1..self.words.len() - 1]
    }
}
impl Drop for Roots {
    fn drop(&mut self) {
        if self.words.len() > 2 {
            // SAFETY: removes exactly the registered range, before its allocation is freed.
            unsafe {
                GC_remove_roots(
                    self.words.as_ptr().add(1) as *mut c_void,
                    self.words.as_ptr().add(self.words.len() - 1)
                        as *mut c_void,
                );
            }
        }
    }
}
/// Force a collection for ABI validation. Live words must be on registered stacks or roots.
pub fn collect() {
    rt_init();
    // SAFETY: the calling thread is registered and the collector owns its heap.
    unsafe {
        GC_gcollect();
    }
}
pub fn constructor(tag: u32, fields: &[Word]) -> Word {
    if fields.is_empty() {
        layout::immediate(tag)
    } else {
        object(layout::CON, tag, fields)
    }
}
pub fn double(n: f64) -> Word {
    let p = rt_alloc_atomic(16).cast::<Word>();
    // SAFETY: a DOUBLE consists of one header and one unscanned f64 word.
    unsafe {
        p.write(layout::header(layout::DOUBLE, 0, 1));
        p.add(1).write(n.to_bits());
    }
    p as Word
}
pub fn string(s: &str) -> Word {
    let payload = 1 + s.len().div_ceil(8);
    let size = u32::try_from(payload).expect("string too large");
    let bytes = (payload + 1).checked_mul(8).expect("string size overflow");
    let p = rt_alloc_atomic(bytes);
    // SAFETY: p contains header, byte length, then padded UTF-8 bytes; no pointers are stored.
    unsafe {
        p.write_bytes(0, bytes);
        p.cast::<Word>()
            .write(layout::header(layout::STRING, 0, size));
        p.cast::<Word>().add(1).write(s.len() as Word);
        ptr::copy_nonoverlapping(s.as_ptr(), p.add(16), s.len());
    }
    p as Word
}
/// # Safety
/// w must point to a live, aligned heap object with a valid header.
pub unsafe fn header(w: Word) -> Word {
    assert!(!layout::is_immediate(w) && w != 0);
    // SAFETY: required live object header is the first word.
    unsafe { (w as *const Word).read() }
}
/// # Safety
/// w must be a live object and index must name a payload word.
pub unsafe fn field(w: Word, index: usize) -> Word {
    // SAFETY: the caller guarantees a live object; bounds are checked before reading.
    unsafe {
        assert!(index < layout::size(header(w)));
        (w as *const Word).add(index + 1).read()
    }
}
/// # Safety
/// w must be a live DOUBLE object.
pub unsafe fn unbox_double(w: Word) -> f64 {
    // SAFETY: the object kind and its fixed-size payload are checked.
    unsafe {
        assert_eq!(layout::kind(header(w)), layout::DOUBLE);
        f64::from_bits(field(w, 0))
    }
}
/// # Safety
/// w must be a live STRING, kept rooted for the returned borrow's entire lifetime.
pub unsafe fn text<'a>(w: Word) -> &'a str {
    // SAFETY: caller maintains the object; header/length/UTF-8 are checked before borrowing.
    unsafe {
        let h = header(w);
        assert_eq!(layout::kind(h), layout::STRING);
        let len = field(w, 0) as usize;
        assert!(
            len <= layout::size(h)
                .checked_sub(1)
                .expect("string length word")
                * 8
        );
        std::str::from_utf8(std::slice::from_raw_parts(
            (w as *const u8).add(16),
            len,
        ))
        .expect("runtime UTF-8")
    }
}
/// Allocate a scanned object, protecting fields even when supplied in a Rust Vec.
pub fn object(kind: u8, tag: u32, fields: &[Word]) -> Word {
    let roots = Roots::new(fields);
    let size = u32::try_from(fields.len()).expect("object too large");
    let p = rt_alloc(fields.len() + 1);
    // SAFETY: p has header + fields words; protected input and destination do not overlap.
    unsafe {
        p.write(layout::header(kind, tag, size));
        ptr::copy_nonoverlapping(
            roots.as_slice().as_ptr(),
            p.add(1),
            fields.len(),
        );
    }
    p as Word
}
}

런타임은 메인 스레드에서 Boehm을 초기화합니다. 외부 Rust 작업자 스레드는 자신의 스택을 등록하고, 종료할 때 등록을 해제합니다. 직접적인 ABI 테스트는 호출 내부에서, 그리고 순환하는 클로저 환경이 살아 있는 동안 수집을 강제로 일으킵니다. 이러한 경우들은 할당이 우연히 수집을 일으키지 않을 때만 동작하는 가정을 드러냅니다.

클로저는 C 호출 규약을 따르는 느린 진입점과 그 구문적 항수를 저장합니다. 이 진입점은 클로저 워드와, 정확히 그 항수만큼의 인자를 가리키는 포인터를 받습니다. 클로저 생성은 단순히 해당 객체를 만듭니다:

#![allow(unused)]
fn main() {
use crate::{
    heap::{self, Roots},
    layout::{self, Word},
};
/// The slow entry receives its closure word and exactly its syntactic arity of arguments.
pub type Slow = unsafe extern "C" fn(Word, *const Word) -> Word;
/// # Safety
/// Arguments must be valid live words of the callee's source types, rooted by the caller.
/// args must point to n readable words (or may be null when n is zero).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rt_make_pap(
    clo: Word,
    n: usize,
    args: *const Word,
) -> Word {
    // SAFETY: caller supplies a readable argument array and a live function value.
    unsafe {
        let args = arguments(n, args);
        let mut fields = vec![clo, n as Word];
        fields.extend_from_slice(args);
        heap::object(layout::PAP, 0, &fields)
    }
}
unsafe fn arguments<'a>(n: usize, args: *const Word) -> &'a [Word] {
    if n == 0 {
        &[]
    } else {
        // SAFETY: the enclosing ABI contract requires n initialized, aligned words.
        unsafe { std::slice::from_raw_parts(args, n) }
    }
}
/// # Safety
/// clo must be a live CLOSURE and index a capture slot; value must be live and well typed.
/// Patching is confined to unpublished letrec initialization; no concurrent readers exist.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rt_closure_set(
    clo: Word,
    index: usize,
    value: Word,
) {
    // SAFETY: caller guarantees exclusive initialization; checked index excludes code/arity.
    unsafe {
        let h = heap::header(clo);
        assert_eq!(layout::kind(h), layout::CLOSURE);
        let i = index
            .checked_add(layout::CLOSURE_CAPTURES)
            .expect("capture overflow");
        assert!(i < layout::size(h));
        (clo as *mut Word).add(i + 1).write(value);
    }
}
/// # Safety
/// clo is a live closure or PAP; args is n valid, rooted, correctly typed words.
/// Every slow entry must obey the runtime ABI and return a live word of its result type.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rt_apply(
    mut clo: Word,
    n: usize,
    args: *const Word,
) -> Word {
    // SAFETY: all raw accesses follow the caller's typed-object contract and checked headers.
    unsafe {
        let mut roots = Roots::new(arguments(n, args));
        loop {
            let h = heap::header(clo);
            match layout::kind(h) {
                layout::PAP => {
                    let count =
                        heap::field(clo, layout::PAP_COUNT) as usize;
                    assert_eq!(layout::size(h), count + layout::PAP_ARGS);
                    let mut all = (0..count)
                        .map(|i| heap::field(clo, layout::PAP_ARGS + i))
                        .collect::<Vec<_>>();
                    all.extend_from_slice(roots.as_slice());
                    roots = Roots::new(&all);
                    clo = heap::field(clo, layout::PAP_CLOSURE);
                }
                layout::CLOSURE => {
                    let arity =
                        heap::field(clo, layout::CLOSURE_ARITY) as usize;
                    assert!(arity > 0);
                    if roots.as_slice().len() < arity {
                        return rt_make_pap(
                            clo,
                            roots.as_slice().len(),
                            roots.as_slice().as_ptr(),
                        );
                    }
                    let address = heap::field(clo, layout::CLOSURE_CODE);
                    // SAFETY: closure construction stores a Slow address, validated by the ABI contract.
                    let code: Slow = std::mem::transmute::<usize, Slow>(
                        address as usize,
                    );
                    let result = code(clo, roots.as_slice().as_ptr());
                    if roots.as_slice().len() == arity {
                        return result;
                    }
                    roots = Roots::new(&roots.as_slice()[arity..]);
                    clo = result;
                }
                _ => panic!("runtime application of non-function"),
            }
        }
    }
}
macro_rules! apply_n { ($name:ident; $($arg:ident),+) => {
    /// Fixed-size apply entry point.
    /// # Safety
    /// The closure and argument words must satisfy rt_apply's typed, live-object contract.
    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn $name(clo: Word, $($arg: Word),+) -> Word {
        let args = [$($arg),+];
        // SAFETY: the local array contains the caller's valid words for the duration of apply.
        unsafe { rt_apply(clo, args.len(), args.as_ptr()) }
    }
}; }
apply_n!(rt_apply1; a);
apply_n!(rt_apply2; a,b);
apply_n!(rt_apply3; a,b,c);
apply_n!(rt_apply4; a,b,c,d);
/// Store a code address, syntactic arity, then captured values.
pub fn closure(code: Slow, arity: usize, captures: &[Word]) -> Word {
    assert!(arity > 0);
    let mut fields = vec![code as usize as Word, arity as Word];
    fields.extend_from_slice(captures);
    heap::object(layout::CLOSURE, 0, &fields)
}
}

rt_apply는 인자 버퍼를 루트로 등록하고, 저장된 PAP 인자와 새 인자를 합친 뒤, 19장의 eval/apply 프로토콜을 반복합니다(Marlow and Peyton Jones 2004). 인자가 모자란 호출은 PAP를 할당하고, 정확한 호출은 코드로 진입하며, 과포화 호출은 반환된 클로저와 남은 인자로 계속 진행합니다. rt_apply1부터 rt_apply4까지는 같은 구현을 사용합니다. rt_closure_set은 아직 공개되지 않은 재귀 그룹의 초기화 중에만 캡처 슬롯을 검사하고 수정합니다.

apply의 루프는 C 느린 진입점을 거치는 네이티브 꼬리 호출 제거를 보장하지 않습니다. 진입점 자체가 다시 apply를 호출할 수 있기 때문입니다. 21장의 알려진 빠른 호출(known fast call)에는 별도의 LLVM 꼬리 호출 규약이 필요합니다. 반복적인 ANF 및 Flat 머신은 이미 재귀적인 호스트 호출을 피하지만, 이는 그 인터프리터들의 성질일 뿐 C ABI가 자동으로 갖는 성질이 아닙니다.

primitives.rs는 안전하고 타입이 있는 값 API를 abi.rs의 워드 ABI와 분리합니다. 세 인터프리터 모두 자신의 데이터 값을 그 안전한 API에 맞춰 변환합니다. 각 인터프리터는 자체 제어 흐름 엔진과 힙 모델을 그대로 유지합니다. 네이티브 쪽 익스포트는 검사된 소스 타입에 따라 워드를 디코딩하고, 공유 의미론을 호출한 뒤, 결과를 인코딩합니다. 안전하지 않은 접근은 런타임 안으로 한정되며, 타입이 지정된 살아 있는 객체에 대한 계약으로 문서화됩니다. extern "C" 진입점을 가로지르는 언와인딩은 중단(abort)됩니다. 반면 언어 차원의 실패는 명시적인 런타임 오류 경로와 종료 경로를 따릅니다.

show_double은 Rust의 최단 왕복(round-trip) 숫자열에서 시작한 뒤 표기법을 정규화합니다. 크기가 1e-4 이상 1e16 미만인 0이 아닌 값은 자릿값 표기법을 사용합니다. 그 밖의 유한한 값은 부호 있는 지수를 사용합니다. 0은 부호를 유지하며, 특수 값은 NaN, Infinity, -Infinity로 표기합니다. 우리는 경계값을 안전한 API와 워드 ABI 양쪽을 통해 검사하며, 유한한 값이 출력 후 다시 읽는 왕복을 거쳐도 같은 비트로 남는지도 확인합니다. readInt는 앞뒤의 선택적 공백과 십진 부호를 허용한 다음, 부호 있는 63비트 범위를 벗어나는 값을 거부합니다. 부동소수점 round는 동률일 때 짝수로 반올림(ties-to-even)하며 포화(saturate)됩니다. NaN은 0으로 변환됩니다.

실행해 보기

더 큰 double-specials 픽스처는 자릿값/과학적 표기법의 경계도 검사합니다:

$ cd crates/ch20_runtime
$ cargo run -q -- run tests/programs/runtime/double-specials.fun --lint
-0.0
NaN
Infinity
-Infinity
0.0001
1.0e+16
$ cargo run -q -- dump --pass=types tests/programs/runtime/double-specials.fun
main : IO ()

저장소 루트에서 cargo test -p fun-rt-20을 실행하면 기본 연산의 의미와 독립 실행형 ABI 하네스를 연습 문제처럼 두루 시험합니다. python3 scripts/check_runtime_abi.py는 정적 런타임을 빌드하고, rustc에 호스트의 네이티브 정적 라이브러리를 물어본 다음, 시스템 컴파일러로 C 스모크 프로그램을 링크하고, 그 프로그램이 42를 출력하는지 확인합니다. just build 역시 워크스페이스보다 먼저 런타임을 명시적으로 빌드합니다. Linux에서는 libgc-dev가 필요합니다. MacOS에서는 Homebrew의 bdw-gc를 사용하며, 다른 설치 위치를 쓰려면 GC_LIB_DIR을 지정할 수 있습니다.

수집기 버그는 할당이 정확히 잘못된 순간에 일어날 때까지 숨어 있는 경우가 많습니다. 런타임 테스트는 재귀 클로저를 만드는 도중과 호출이 살아 있는 인자를 붙들고 있는 동안에 수집을 강제로 일으킵니다. 이 테스트들은 부분 호출과 과포화 호출, 유니코드 문자열, 오류 전 출력도 연습 문제처럼 두루 시험합니다. 이런 곳들이야말로 겉보기에 올바른 객체 레이아웃이 실행 중인 프로그램의 일부로서 제대로 동작해야 하는 지점입니다.

연습 문제

  1. 기초. -1, 0, True, unit에 대한 인코딩된 워드를 계산하십시오. 같은 워드가 반드시 같은 소스 값을 나타내지는 않는 이유를 설명하십시오.
  2. 중급. 필드가 두 개인 생성자와 인자가 하나인 PAP를 헤더와 페이로드 크기를 포함해 그려 보십시오.
  3. 중급. 일반적인 Rust 벡터에만 저장된 살아 있는 포인터가 Boehm의 시야에서 사라질 수 있는 이유를 설명하십시오.
  4. 고급. ABI 테스트에서 재귀 클로저 할당과 패치 사이에 강제 수집을 추가하되, 완성되지 않은 모든 객체가 루트로 유지되도록 하십시오.
  5. 고급. 이동 수집기가 클로저 캡처, PAP 인자, 전역 변수, 살아 있는 지역 값을 갱신하려면 어떤 추가 메타데이터가 필요한지 밝히십시오.

더 읽을거리

  • 컴파일러의 협조 없이 이루어지는 보수적 수집 (Boehm and Weiser 1988).
  • 컴파일러의 협력을 통한 루트 탐색 (Henderson 2002).
  • 엄격한 함수형 런타임에서의 표현 방식 선택 (Leroy 1990).
  • Eval/apply와 부분 적용 (Marlow and Peyton Jones 2004).
  • 수집 알고리즘과 구현상의 트레이드오프 (Jones et al. 2023).
  • Takenobu Tani의 GHC (STG, Cmm, asm) Illustrated(pdf / github)는 GHC의 힙 객체, 스택 레이아웃, 호출 규약을 우리 것과 나란히 비교해 보기에 좋습니다.

21장: LLVM 코드 생성

새 파일

  • src/llvm/mod.rs
  • src/llvm/control.rs
  • src/llvm/values.rs
  • src/llvm/primitives.rs
  • src/native.rs

변경된 파일

  • src/flat.rs
  • build.rs
  • src/flat_lint.rs
  • src/lib.rs
  • src/main.rs

동기

이제 우리는 Fun 프로그램을 실행할 수 있지만, 실제 작업은 여전히 우리의 인터프리터가 맡아야 합니다. 이 장에서는 셸에서 바로 실행할 수 있는 실행 파일을 만들어 보겠습니다. 계산기에서 출발한 프로젝트로서는 꽤 만족스러운 도착점입니다!

준비 작업의 상당 부분은 이미 끝났습니다. 패턴 매칭은 분기가 되었고, 클로저는 명시적인 환경을 갖게 되었으며, Flat IR은 호출과 꼬리 전달이 어디에 위치해야 하는지 알려 줍니다. 명령어 선택, 레지스터 할당, 기계어 수준 최적화는 LLVM이 처리할 것입니다.

그래도 우리 값이 메모리에 어떻게 배치되는지, 우리 함수들이 서로를 어떻게 호출하는지는 여전히 우리가 LLVM에 알려 주어야 합니다. 이러한 결정이 런타임과 어긋나면, LLVM은 잘못된 프로그램을 효율적으로 컴파일해 버릴 수 있습니다. 우리는 이미 이해하고 있는 표현들을 활용해 새로운 작업을 감당할 만한 수준으로 유지하면서, 그 경계를 신중하게 짚어 나가겠습니다.

현재의 언어

언어 자체는 바뀌지 않았습니다. fun-ch21 build FILE -o OUT은 실행 파일을 만듭니다. run은 여전히 인터프리터를 선택합니다. -O0부터 -O3까지가 최적화를 제어하며, 기본값은 -O2입니다. --dump=llvm은 최적화 전의 모듈을, --dump=llvm-opt는 최적화 후의 모듈을 보여 줍니다.

인수 테스트 프로그램은 레코드, 사전, 고차 함수, IO, 그리고 엄격한 누산기 루프를 한데 모읍니다:

-- engines: native
data Shape = Circle Double | Rect Double Double
  deriving (Show)

data Point = Point { x :: Double, y :: Double }
  deriving (Eq, Show)

class Area a where
  area :: a -> Double

instance Area Shape where
  area (Circle r) = 3.0 * r * r
  area (Rect w h) = w * h

norm :: Point -> Double
norm p = sqrt (p.x * p.x + p.y * p.y)

sumTo :: Int -> Int
sumTo n = go 0 n
  where
    go acc 0 = acc
    go acc k = go (acc + k) (k - 1)

main :: IO ()
main = do
  let p = Point { x = 3.0, y = 4.0 }
  print (norm p)
  mapM_ (print . area) [Circle 1.0, Rect 2.0 3.0]
  print (map (.x) [p, p { x = 10.0 }])
  print (p == p { y = 4.0 })
  print (sumTo 10000000)

마지막 호출은 천만 번 반복을 수행합니다. 올바른 수를 얻는 것은 과제의 일부일 뿐입니다. 우리의 명시적 기계에서 꼬리 호출이 그러하듯이, 네이티브 스택 역시 유계(bounded)로 유지되어야 합니다.

이론

LLVM은 정적 단일 대입(static single assignment), 곧 SSA를 사용합니다. 각 명령어 결과에는 정의가 하나뿐이므로, 이름은 언제나 같은 계산된 값을 가리킵니다. 우리의 고유한 Flat 바인더는 이미 이 개념에 잘 들어맞습니다. Let은 바인더를 LLVM 값에 연결하고, Join은 여러 경로가 들어올 수 있는 기본 블록이 됩니다.

흥미로운 질문은 그 경로들이 블록에 값을 어떻게 전달하느냐입니다. LLVM은 phi 노드를 사용합니다. 각 조인 매개변수는 phi가 되고, Jump는 블록으로 분기하기 전에 자신의 유입 값을 제공합니다. 우리가 이미 만들어 둔 조인 포인트 덕분에 이 변환은 자연스러운 형태를 갖게 됩니다 (Maurer et al. 2017).

하나의 조인에서 만나는 두 경로를 생각해 봅시다. 한 경로는 10을, 다른 경로는 20을 제공합니다. phi는 우리가 어느 경로로 도착했는지에 따라 그 경로에 연결된 값을 취합니다. 원래의 조건을 다시 검사하지는 않습니다.

여기에는 코드 생성에서 저지르기 쉬운 실수가 하나 있습니다. Flat 연산 하나를 방출하면 LLVM 블록이 여러 개 생길 수 있습니다. 예를 들어 나눗셈은 0 검사를 추가합니다. phi의 유입 간선은 식을 방출하기 시작한 블록이 아니라, 그 검사 후에 실제로 떠나는 블록을 가리켜야 합니다. 점프 인자를 내보낸 뒤에는 빌더의 현재 블록을 기록하십시오.

Flat 구성 요소LLVM 변환
Let명령어와 이름-값 항목
Case태그 검사와 switch
Joinphi 매개변수를 갖는 블록
Jumpphi 유입 값과 br
알려진 꼬리 호출tail call tailcc, 바로 뒤에 ret
알려지지 않은 호출C 호출 규약 런타임 apply

꼬리 위치만으로는 네이티브 스택 사용량을 유계로 유지하기에 충분하지 않습니다. LLVM은 호환되는 호출 규약과 적절한 반환 시퀀스를 요구합니다. 우리는 모든 빠른 진입점에 tailcc를 부여하고, 알려진 꼬리 호출에 표시를 하며, 호출 결과를 즉시 반환합니다. 내부 링크(internal linkage)는 x86-64 위치 독립 코드의 가시성 요구 사항도 충족합니다. 이러한 선택은 LLVM의 꼬리 호출 규칙을 따릅니다. 이 보장은 상호 재귀를 포함하여 알려진 포화 호출에 적용됩니다. 이 보장은 런타임의 C eval/apply 경계를 넘어서까지 확장되지는 않습니다.

구현

먼저 타깃을 확정하기

llvm/mod.rs는 모듈, 빌더, 타깃 머신, 심볼 테이블을 소유합니다. 이 모듈의 LLVM 값들은 호출자가 소유한 Context를 빌려 씁니다. 이렇게 하면 수명 경계가 명시적으로 드러나고 자기 참조적인 Rust 구조체를 피할 수 있습니다. 모든 함수 진입점과 전역 슬롯은 본문보다 먼저 선언되므로, 전방 호출과 상호 재귀 호출에 특별한 처리가 필요하지 않습니다.

이 장은 Inkwell 0.10.0을 llvm22-1-prefer-dynamic과 함께 LLVM 22로 고정합니다. 타깃 초기화는 한 번만 실행됩니다. 타깃 머신은 호스트 트리플, 범용(generic) CPU, 위치 독립 재배치를 사용합니다. 이 타깃 머신의 트리플과 데이터 레이아웃은 방출 이전에 모듈에 설정됩니다. 32비트 포인터 타깃은 거부됩니다. 바인딩을 더 간단히 소개하는 자료로는 컴파일러 크레이트의 예제를 참고하십시오.

빌더 연산은 오류를 반환합니다. Flat 린트는 입력을 검사하고, LLVM의 검증기는 --lint와 관계없이 완성된 모든 모듈을 검사합니다. 최적화는 새 패스 관리자를 사용하며 다시 한 번 검증합니다:

#![allow(unused)]
fn main() {
/// Run the new pass manager only after constructing a complete, valid module.
pub fn optimize(&self, level: u8) -> Result<()> {
    if level > 3 {
        return Err(Error("optimization level must be 0..3".into()));
    }
    self.module
        .run_passes(
            &format!("default<O{level}>"),
            &self.machine,
            PassBuilderOptions::create(),
        )
        .map_err(|e| Error(e.to_string()))?;
    self.verify()
}
}

검증기는 누락된 종결자, 잘못된 phi, 타입이 맞지 않는 호출을 잡아낼 수 있습니다. 이는 훌륭한 1차 방어선입니다. 하지만 완벽하게 유효한 LLVM이라도 Fun을 잘못 번역한 결과일 수 있습니다. 예를 들어 인자를 잘못된 순서로 평가하거나 정수 오버플로를 잘못 처리할 수 있습니다. 우리는 같은 프로그램을 인터프리터와 네이티브 코드 양쪽으로 실행하여 이러한 실수를 잡아냅니다.

작업 목록으로 순회하기

방출기는 Flat 트리를 결코 재귀적으로 내려가지 않습니다. 대기 중인 블록은 벡터에 저장되며, 루프가 직선형 Let 척추를 하나씩 소비합니다. 종결 처리기는 반환이나 제어 이전을 방출하거나, 조인과 대안의 본문을 큐에 넣습니다.

환경은 고유한 SSA 정의들의 테이블입니다. 렉시컬 스코프는 Flat 린트에서 이미 검사했으므로, 방출 단계에서는 분기마다 스코프 맵을 복제하지 않습니다. 조인의 phi는 그 호출자들을 방문하기 전에 생성됩니다. 들어오는 간선은 그 호출자들이 방출될 때 추가할 수 있습니다. 완성된 각 블록에는 종결자가 정확히 하나 있습니다.

작업 목록 방식은 린트, 출력, 정리에도 적용됩니다. 이 중 마지막 것은 잊기 쉽습니다. Rust가 생성하는 소멸자는 컴파일이 겉보기에 성공한 뒤에도 깊은 Rc 트리를 재귀적으로 해제할 수 있습니다. 우리는 이 간선들도 작업 목록으로 비워 냅니다. 깊은 Flat 트리는 작은 Rust 스택 위에서 검사되고, 출력되고, 방출되고, 검증되고, 해제됩니다. 이렇게 하면 코드 생성 루프가 성공했다고 해서 다른 모든 패스가 안전하다고 가정하는 대신, 우리의 Flat 순회를 직접 검사하게 됩니다.

#![allow(unused)]
fn main() {
pub(super) fn body(
    &mut self,
    b: &Body,
    mut env: Env<'ctx>,
    mut joins: Joins<'ctx>,
) -> Result<()> {
    // Unique binders index SSA values; lint already checked scopes.
    // Branches share this table without cloning lexical environments.
    let block = self.builder.get_insert_block().unwrap();
    let mut work = vec![Block {
        body: b.clone(),
        block,
    }];
    while let Some(Block { body, block }) = work.pop() {
        self.builder.position_at_end(block);
        let mut cursor = body.as_ref();
        while let Flat::Let(v, r, rest) = cursor {
            let value = self.rhs(r, &env)?;
            env.insert(v.clone(), value);
            cursor = rest;
        }
        self.terminal(cursor, &mut env, &mut joins, &mut work)?;
    }
    Ok(())
}
}

워드 표현 유지하기

모든 Fun 값은 20장의 공유 runtime_layout을 사용하는 LLVM i64입니다. 로드는 워드를 포인터로 변환하고 워드 배열을 인덱싱합니다. Inkwell의 불투명 포인터는 접근하는 원소 타입을 명시적으로 요구합니다:

#![allow(unused)]
fn main() {
pub(super) fn slot(
    &self,
    pointer: PointerValue<'ctx>,
    index: u64,
) -> Result<PointerValue<'ctx>> {
    // SAFETY: GEP indexes i64 words using the checked ABI layout.
    Ok(unsafe {
        self.builder.build_gep(
            self.word,
            pointer,
            &[self.n(index)],
            "slot",
        )?
    })
}
}

unsafe 경계는 LLVM을 구성합니다. 이 경계는 Rust 포인터를 역참조하지 않습니다. 그래도 생성된 접근 코드는 검사를 거친 생성자 항수와 런타임 레이아웃에 의존합니다. 우리는 추가적인 inbounds 보장을 주장하지 않고 일반 GEP를 사용합니다. 생성자 필드는 헤더에서 한 워드 뒤에 있습니다. 클로저 캡처는 여기에 더해 느린 진입점 주소와 항수도 건너뜁니다.

인자 없는 생성자는 즉칫값입니다. 그 밖의 생성자는 rt_alloc으로 할당한 다음 슬롯 순서대로 초기화합니다. 두 형태를 모두 포함하는 타입에 대한 case는 헤더를 로드하기 전에 최하위 비트를 검사합니다. 한 형태만 포함하는 타입에는 그런 분기가 필요하지 않습니다. 정수 및 문자 리터럴 case는 태그가 붙은 워드에 대해 곧바로 switch합니다. 문자열 동등성은 매치 컴파일 단계에서 이미 기본 검사로 바뀌었습니다.

문자열 리터럴은 헤더, 바이트 길이, 패딩된 UTF-8 바이트를 담은 정렬된 읽기 전용 객체입니다. 유지되는 데이터 테이블이 이들의 루트 워드를 기록합니다. llvm.used는 최적화와 링킹을 거치는 동안 그 테이블을 유지합니다. 이 정적 객체들은 결코 이동되거나 해제되지 않습니다. 힙 문자열과 double은 포인터를 담지 않으므로 아토믹 할당을 사용합니다. 생성된 전역 변수와 살아 있는 기계 레지스터/스택 슬롯은 Boehm에게 계속 보입니다 (Boehm and Weiser 1988).

정수 연산은 산술 시프트로 디코딩하고, i64로 계산한 뒤, 다시 태그를 붙입니다. 랩어라운드는 정의된 동작이므로 우리는 nsw도 nuw도 붙이지 않습니다. 제수가 0이면 나눗셈 전에 rt_div_zero로 분기합니다. 가장 작은 Fun 정수를 -1로 나눈 결과는 더 넓은 중간 i64에 들어갑니다. 다시 태그를 붙이는 과정이 필요한 63비트 랩어라운드를 제공합니다. div와 mod는 부호 때문에 내림(flooring)이 필요할 때 버림 방식의 몫과 나머지를 보정합니다.

Double 연산은 박스를 풀고, fast-math 플래그 없이 IEEE 명령어를 사용한 뒤, 다시 박싱합니다. 순서 있는(ordered) 비교는 NaN 동작을 보존합니다. 초월 함수, 포화 변환, 텍스트, 서식화, IO는 인터프리터가 사용하는 것과 같은 런타임을 호출합니다. LLVM 최적화가 이러한 언어 수준의 선택을 대체해서는 안 됩니다.

빠른 호출과 클로저 적용 분리하기

끌어올려진 함수마다 진입점이 두 개 있습니다. 빠른 진입점은 tailcc로 환경 워드 하나와 그 뒤에 오는 일반 워드 인자들을 받습니다. 최상위 함수는 사용하지 않는 환경 자리에 0을 받습니다. 알려진 호출은 피호출자뿐 아니라 호출 명령어에도 호출 규약을 지정합니다:

#![allow(unused)]
fn main() {
pub(super) fn known(
    &self,
    f: FuncId,
    env: Option<IntValue<'ctx>>,
    args: &[IntValue<'ctx>],
    tail: bool,
) -> Result<IntValue<'ctx>> {
    let mut actual: Vec<BasicMetadataValueEnum> =
        vec![env.unwrap_or(self.n(0)).into()];
    actual
        .extend(args.iter().map(|v| BasicMetadataValueEnum::from(*v)));
    let call = self.builder.build_call(
        self.fast[f.0 as usize],
        &actual,
        "known",
    )?;
    call.set_call_convention(TAIL_CC);
    call.set_tail_call(tail);
    if self
        .builder
        .get_insert_block()
        .unwrap()
        .get_parent()
        .unwrap()
        .get_call_conventions()
        != TAIL_CC
    {
        call.set_tail_call_kind(
            inkwell::values::LLVMTailCallKind::LLVMTailCallKindNoTail,
        );
    }
    Ok(call.try_as_basic_value().unwrap_basic().into_int_value())
}
}

느린 진입점은 C ABI를 사용하며, 클로저 워드와 인자 배열 포인터를 받습니다. 이 진입점은 함수의 구문적 항수만큼 정확히 인자를 풀어낸 뒤 빠른 코드로 진입합니다. 클로저는 이 느린 진입점의 주소를 저장합니다. 알려지지 않은 호출은 rt_apply1부터 rt_apply4까지를 사용하며, 더 넓은 묶음에는 범용 rt_apply를 사용합니다. 일반 인자 배열은 제어 흐름 지점에서 반복적으로 할당하지 않고 함수의 진입 블록에서 할당합니다. 런타임은 기존의 eval/apply 프로토콜을 사용하여 부분 적용과 과포화 적용을 처리합니다(Marlow and Peyton Jones 2004).

C 어댑터는 보장된 꼬리 전달이 아니며, rt_apply를 거치는 알 수 없는 꼬리 호출도 마찬가지입니다. 따라서 고차 재귀는 네이티브 스택을 키울 수 있습니다. 이 보장을 확장하려면 호환되는 호출 규약을 갖춘, LLVM이 생성하는 apply 경로가 필요합니다. 선택적인 최적화기 변환을 언어 차원의 보장처럼 취급하기보다는 이 경계를 명시적으로 유지하는 편이 더 유용합니다.

초기화, 생성, 링크

fun_init은 먼저 전역 함수 클로저를 설치한 다음, 엄격한 값 초기화식을 의존성 순서대로 평가합니다. 생성된 C main은 런타임을 초기화하고, fun_init을 호출하고, 소스의 main을 unit에 적용한 뒤, 출력을 플러시하기 위해 rt_exit을 통해 종료합니다. 이 순서는 어떤 최상위 오류가 먼저 발생하는지를 보존하며, IO 동작을 단지 구성하는 것만으로는 어떤 효과도 일어나지 않도록 보장합니다.

native.rs는 타깃 머신으로 오브젝트 파일을 쓴 다음 시스템 cc를 직접 호출합니다. 이때 libfun_rt_21.a, Boehm, 그리고 rustc가 알려 주는 네이티브 라이브러리 플래그를 함께 링크합니다. FUN_RT로 설치된 아카이브를 지정할 수 있습니다. 지정하지 않으면 컴파일러 옆에서 아카이브를 찾습니다. just build는 런타임과 플래그를 먼저 준비합니다. 링크는 출력 위치 옆의 임시 디렉터리에서 이루어지며, 성공했을 때만 실행 파일을 설치합니다. 셸 보간은 쓰지 않으며, 링크가 실패해도 기존 출력물은 그대로 남습니다.

tailcc와 C 런타임 사이를 오가는 호출에는 명시적으로 notail을 붙입니다. 그렇게 하지 않으면 LLVM이 서로 다른 두 호출 규약에 걸쳐 꼬리 전달을 추론하여, 인자가 많은 호출에서 스택 정리를 잘못할 수 있습니다. IR이 검증을 통과할 수도 있기 때문에 이것은 특히 고약한 버그입니다. 이 경계를 표시해도 이미 알려진 Fun 함수 사이에서 우리가 보장하는 꼬리 호출에는 영향이 없습니다.

실행해 보기

저장소 README의 설명대로 LLVM 22와 Boehm을 설치하십시오. LLVM_SYS_221_PREFIX를 LLVM 설치 경로(prefix)로 설정한 다음 루트에서 빌드하십시오:

$ just build
$ target/debug/fun-ch21 build crates/ch21_llvm/tests/programs/native/acceptance.fun -o build/acceptance --lint
$ build/acceptance
5.0
3.0
6.0
[3.0,10.0]
True
50000005000000
$ target/debug/fun-ch21 dump --pass=llvm crates/ch21_llvm/tests/programs/native/acceptance.fun > build/acceptance.ll
$ target/debug/fun-ch21 dump --pass=llvm-opt crates/ch21_llvm/tests/programs/native/acceptance.fun > build/acceptance-opt.ll

예제를 네이티브 코드와 인터프리터 양쪽으로 실행해 보십시오. 평가 순서 버그는 어떤 오류가 먼저 나타나는지에서 드러나는 경우가 많으므로, 성공한 결과뿐 아니라 오류도 비교하십시오. 모든 최적화 수준을 시험해 보는 것도 좋습니다. 호출 규약 실수는 LLVM이 특정 변환을 하기로 결정할 때까지 숨어 있을 수 있습니다.

별도의 스트레스 테스트는 네이티브 재귀 프로그램을 512 KiB 스택으로 실행합니다. 또 다른 루프는 64 MiB의 Boehm 힙 한도 아래에서 최소 1억 워드를 할당합니다. 이 테스트들은 함께 서로 다른 두 가지 의무를 검사합니다. 꼬리 전달은 호출 프레임을 해제해야 하고, 죽은 힙 객체는 계속 회수 가능해야 합니다. LLVM 검증을 통과했다는 것만으로는 둘 중 어느 것도 입증되지 않습니다.

연습 문제

  1. 몸풀기. 태그가 붙은 워드에서 시작해 inttoptr, GEP, load를 거치는 필드 로드를 추적하십시오. 앞선 어떤 검사가 그 필드 인덱스를 정당화하는지 밝히십시오.
  2. 중급. 호출자가 둘인 조인의 블록을 그리십시오. 한쪽 호출자에 나눗셈을 추가한 뒤, 올바른 phi 선행자가 무엇인지 찾아보십시오.
  3. 중급. 정수 곱셈에 nsw를 붙이면 대부분의 예제가 여전히 통과하더라도 Fun의 의미론이 바뀌는 이유를 설명하십시오.
  4. 고급. sumTo의 최적화 전 IR과 최적화된 IR을 비교하십시오. 재귀 전달의 양쪽 끝에서 호출 규약을 확인하십시오.
  5. 고급. C apply 경계를 드러내는 미지 호출(unknown call) 스트레스 테스트를 설계하십시오. 이를 보장된 꼬리 호출로 만들려면 ABI를 어떻게 바꿔야 하는지 설명하십시오.

더 읽을거리

22장: JIT와 REPL

새 파일

  • src/jit.rs
  • src/repl.rs
  • src/bin/fun.rs

변경된 파일

  • src/llvm/mod.rs
  • src/llvm/control.rs
  • rt/src/heap.rs
  • src/session.rs
  • src/rename.rs
  • build.rs
  • src/core.rs
  • src/core_type.rs
  • src/elaborate.rs
  • src/erase.rs
  • src/lib.rs
  • src/main.rs

동기

실행 파일은 컴파일러가 종료된 뒤에도 실행될 수 있습니다. JIT를 사용하면 컴파일러를 계속 살려 두고, 생성된 코드를 컴파일러 프로세스 안에서 실행합니다. 덕분에 다음으로 만들어 볼 꽤 재미있는(fun) 것이 생깁니다. 바로 우리가 입력하는 정의를 컴파일하는 REPL입니다.

LLVM은 이미 기계어 명령어를 생성하는 방법을 알고 있습니다. 까다로운 부분은 프로그램이 그 명령어를 필요로 하는 동안 계속 살아 있도록 유지하는 것입니다. saved = make 40을 입력한 다음, 다음 줄에서 saved 2를 호출해 보십시오. 그 클로저는 캡처한 정수와 이전 입력의 코드 안에 있는 주소를 함께 담고 있을 수 있습니다. 우리는 둘 다 유지해야 합니다.

우리는 21장의 컴파일러 파이프라인과 호출 규약을 그대로 유지할 것입니다. 그 주위에, 받아들인 정의를 기억하고 새 입력이 실패해도 그 정의를 온전히 보존하는 세션을 만들 것입니다. 다음 줄의 오타 하나 때문에 우리가 이미 해 둔 작업이 망가져서는 안 됩니다.

현재의 언어

새로운 Fun 구문은 없습니다. REPL은 식과 선언을 받습니다. 명시적인 중괄호를 사용하면 여러 선언을 하나의 입력에 넣을 수 있습니다. :load로 불러온 파일에서는 일반적인 여러 줄 레이아웃을 그대로 쓸 수 있습니다.

make x = \y -> x + y

saved = make 40

saved 2를 입력하면 42가 출력됩니다. 순수한 식은 Prelude의 print로 감싸지므로, 그 타입에는 Show 인스턴스가 있어야 합니다. IO 식은 그 액션을 실행하고 결과는 버립니다. 함수는 출력하려 하지 말고 :t로 살펴보십시오.

명령의미
:t EXPR주 한정 타입(principal qualified type)을 추론합니다. :type은 별칭입니다.
:k TYPE생성자나 타입 적용의 카인드를 추론합니다.
:core EXPR, :anf EXPR, :llvm EXPR실행하지 않고 중간 프로그램을 살펴봅니다.
:load FILE검사와 초기화가 성공하면 정의를 교체합니다.
:reset유지하던 코드, 루트, 정의를 해제합니다.
:engine NAMEjit, core, anf, flat 중 하나를 고릅니다. 정의는 유지됩니다.
:quit세션을 떠납니다. :q는 별칭입니다.

파일을 불러와도 그 파일의 main은 실행되지 않습니다. 실행하려면 main을 직접 입력하십시오. 소스 파일에서와 마찬가지로 사용자 정의가 중복되면 오류입니다. 이를 바꾸려면 초기화하거나 대체할 파일을 불러오십시오. 컴파일러 오류가 나도 이미 받아들여진 정의는 그대로 남습니다. Prelude를 재정의하는 내용은 첫 번째 선언 블록이나 불러온 파일에 넣으십시오. 나중 입력은 앞선 정의에서 보이는 이름을 바꿀 수 없습니다. 결합성은 같은 입력 안에서 해당 연산자 정의와 함께 주어져야 합니다.

이론

우리 표현이 캡처한 필드 여러 개를 저장하더라도, 클로저는 코드와 환경의 쌍입니다 (Marlow and Peyton Jones 2004). 환경의 도달 가능성은 수명 문제의 절반에 불과합니다. 코드 주소 역시 계속 실행 가능한 상태로 남아 있어야 합니다. Boehm 수집기는 힙 도달 가능성은 이해하지만, LLVM의 기계어 코드를 소유하지 않으며 실행 엔진을 언제 폐기해도 되는지도 알지 못합니다 (Boehm and Weiser 1988).

우리는 세션에 그 세션의 모든 실행 엔진에 대한 소유권을 줍니다. 각 엔진은 입력 하나의 모듈을 소유합니다. 별도의 테이블이 등록된 루트 슬롯에 전역 워드를 보관합니다. 따라서 앞선 모듈은 불변으로 남을 수 있고, 나중 모듈은 외부 전역 선언을 통해 그 값들을 가져옵니다. 초기화는 슬롯 등록을 해제하기 전에 엔진을 파괴합니다. 이들을 감싸는 LLVM Context는 둘보다 오래 살아남습니다.

잘못된 정의를 입력했다고 해서 이미 입력한 올바른 정의들이 망가져서는 안 됩니다. 그래서 우리는 후보를 따로 준비하고, 검사와 초기화가 성공한 뒤에만 그것을 유지합니다. 검사용 명령은 초기화 전에 멈춥니다. error "x"의 타입을 묻는 것은 안전합니다. 그것을 실행하는 것은 전혀 다른 문제입니다. 이 경로들을 분리해 두면 프로그램을 실수로 실행하지 않고도 탐색할 수 있습니다 (Pierce 2002).

구현

반환하는 진입점

네이티브 실행 파일은 main에서 시작해 전역을 초기화하고, IO 동작을 실행한 뒤 종료합니다. REPL은 입력이 성공할 때마다 종료할 수는 없습니다. 그 대신 JIT 로워링은 C에서 호출할 수 있는 void fun_entry(void)를 내보냅니다. 이 함수는 새 전역을 초기화하고, 질의 동작을 unit에 적용한 다음, 반환합니다. 빠른 진입점은 여전히 tailcc를 사용합니다. 클로저 호출도 여전히 같은 C eval/apply 경로를 사용합니다. 그 ABI 경계를 넘는 호출은 명시적으로 notail입니다. 넓은 호출(wide-call) 회귀 테스트에서 LLVM이 tailcc 함수에서 C apply로의 꼬리 전달을 추론하는 문제를 잡아냈는데, 이 문제는 AArch64에서 피호출자가 정리하는(callee-pop) 반환 프레임을 손상시켰습니다. 검증기는 이 타깃 수준의 실패를 잡아내지 못했습니다. 잡아낸 것은 같은 프로그램을 두 백엔드에서 모두 실행해 본 것이었습니다. 이 수정은 21장에도 적용됩니다.

Backend::for_jit는 이 진입점과 외부 전역 저장소를 선택합니다. 이 백엔드는 21장의 작업 목록(worklist) 방출, 검증기, 패스 관리자를 재사용합니다. 대화형 경로에서는 O1을 실행합니다. 네이티브 build는 O0–O3 선택지를 그대로 유지합니다.

#![allow(unused)]
fn main() {
/// A returning entry and externally rooted globals replace the native process startup.
pub fn for_jit(
    context: &'ctx Context,
    program: &'program flat::Program,
    inherited: BTreeSet<Id>,
) -> Result<Self> {
    Self::create(context, program, 1, true, inherited)
}
}

실행 전에 심볼 해석하기

네이티브 오브젝트의 런타임 호출은 시스템 링커가 해석합니다. MCJIT는 같은 주소를 컴파일러 내부에서 필요로 합니다. runtime_symbols는 링크된 런타임의 C 내보내기(export) 주소를 가져와 이것들이 바이너리에 남아 있도록 합니다. map_symbols는 모든 외부 함수를 확인하고 add_global_mapping으로 그 주소를 설치합니다. 이전의 빠른 진입점과 느린 진입점은 같은 단계에서 보존된 각자의 엔진에 매핑됩니다. LLVM 내장 함수(intrinsic)는 계속 LLVM이 책임집니다.

#![allow(unused)]
fn main() {
use crate::{flat, llvm::Backend};
use fun_rt::heap::GlobalRoot;
use inkwell::{
    OptimizationLevel, context::Context, execution_engine::ExecutionEngine,
};
use std::collections::BTreeMap;
type GlobalKey = (String, usize);
/// Keep every engine alive until no REPL binding can reference its closures.
/// Fields drop in declaration order: release code before unregistering the slots.
pub struct Jit<'ctx> {
    engines: Vec<ExecutionEngine<'ctx>>,
    roots: BTreeMap<GlobalKey, GlobalRoot>,
    queries: Vec<GlobalRoot>,
    code: BTreeMap<GlobalKey, (usize, usize, usize)>,
    types: BTreeMap<GlobalKey, Vec<String>>,
    context: &'ctx Context,
}
impl<'ctx> Jit<'ctx> {
    pub fn new(context: &'ctx Context) -> Self {
        fun_rt::rt_init();
        Self {
            engines: vec![],
            roots: BTreeMap::new(),
            queries: vec![],
            code: BTreeMap::new(),
            types: BTreeMap::new(),
            context,
        }
    }
    /// Compile a checked program, import accepted globals, then initialize only new bindings.
    /// Runtime errors retain the native runtime's process-exit semantics.
    pub fn run(&mut self, program: &flat::Program) -> Result<(), String> {
        let mut occurrences = BTreeMap::new();
        let mut keys = BTreeMap::new();
        for global in &program.globals {
            let count =
                occurrences.entry(global.name.text.clone()).or_insert(0);
            keys.insert(
                global.name.clone(),
                (global.name.text.clone(), *count),
            );
            *count += 1;
        }
        let mut checked_types = BTreeMap::new();
        for (id, shape) in &program.metadata.global_types {
            let Some(key) = keys.get(id) else { continue };
            if let Some(previous) = self.types.get(key)
                && previous != shape
            {
                return Err(format!(
                    "input changes the accepted type of {}; use :reset or :load",
                    key.0
                ));
            }
            checked_types.insert(key.clone(), shape.clone());
        }
        let inherited = keys
            .iter()
            .filter(|(_, key)| self.roots.contains_key(*key))
            .map(|(id, _)| id.clone())
            .collect();
        let backend = Backend::for_jit(self.context, program, inherited)
            .map_err(|e| e.to_string())?;
        backend.optimize(1).map_err(|e| e.to_string())?;
        let engine = backend
            .module
            .create_jit_execution_engine(OptimizationLevel::Less)
            .map_err(|e| format!("JIT setup failed: {e}"))?;
        let mut imports = vec![];
        for global in &program.globals {
            if let Some(f) = global.function
                && let Some((fast, slow, arity)) =
                    self.code.get(&keys[&global.name])
            {
                if *arity != program.funcs[f.0 as usize].params.len() {
                    return Err(format!(
                        "input changes the accepted arity of {}; use :reset or :load",
                        global.name.text
                    ));
                }
                imports.push((format!("fun.f{}", f.0), *fast));
                imports.push((format!("fun.f{}.slow", f.0), *slow));
            }
        }
        map_symbols(&backend.module, &engine, &imports)?;
        let mut pending = BTreeMap::new();
        for global in &program.globals {
            let name = &keys[&global.name];
            let slots = if let Some(slots) = self.roots.get(name) {
                slots
            } else {
                pending
                    .entry(name.clone())
                    .or_insert_with(|| GlobalRoot::new(0))
            };
            let value = backend
                .module
                .get_global(&format!("fun.g{}", global.name.name.unique));
            let Some(value) = value else { continue }; // O1 removed an unused declaration.
            engine.add_global_mapping(&value, slots.as_ptr() as usize);
        }
        let mut pending_code = BTreeMap::new();
        for global in &program.globals {
            let key = &keys[&global.name];
            if let Some(f) = global.function
                && !self.code.contains_key(key)
            {
                let fast = engine
                    .get_function_address(&format!("fun.f{}", f.0))
                    .map_err(|e| e.to_string())?;
                let slow = engine
                    .get_function_address(&format!("fun.f{}.slow", f.0))
                    .map_err(|e| e.to_string())?;
                pending_code.insert(
                    key.clone(),
                    (fast, slow, program.funcs[f.0 as usize].params.len()),
                );
            }
        }
        call_entry(&engine)?;
        // A transient entry gets a fresh slot on every input; retain even that slot
        // while its module exists, so no relocation can ever address freed storage.
        if let Some(query) =
            pending.remove(&keys[&program.metadata.main.name])
        {
            self.queries.push(query);
        }
        let query = &keys[&program.metadata.main.name];
        pending_code.remove(query);
        checked_types.remove(query);
        self.code.append(&mut pending_code);
        self.types.append(&mut checked_types);
        self.roots.append(&mut pending);
        self.engines.push(engine);
        Ok(())
    }
    /// Introspection for tests: accepted globals survive later inputs without reinitialization.
    pub fn global_word(&self, name: &str) -> Option<u64> {
        self.roots
            .iter()
            .rfind(|((text, _), _)| text == name)
            .map(|(_, root)| root.get())
    }
}
/// One explicit symbol table links the JIT to the very same ABI as native objects.
pub fn runtime_symbols() -> Vec<(&'static str, usize)> {
    macro_rules! entries {
        ($module:path => [$($name:ident),* $(,)?]) => {{
            use $module as runtime;
            vec![$((stringify!($name), runtime::$name as *const () as usize)),*]
        }};
    }
    let mut symbols = entries!(fun_rt => [
        rt_init,
        rt_alloc,
        rt_alloc_atomic,
        rt_apply,
        rt_apply1,
        rt_apply2,
        rt_apply3,
        rt_apply4,
        rt_closure_set,
        rt_make_pap,
        rt_div_zero,
        rt_error,
        rt_exit,
        rt_pattern_fail,
    ]);
    symbols.extend(entries!(fun_rt::abi => [
        rt_prim_intAdd,
        rt_prim_intSub,
        rt_prim_intMul,
        rt_prim_intNeg,
        rt_prim_intQuot,
        rt_prim_intRem,
        rt_prim_intDiv,
        rt_prim_intMod,
        rt_prim_intEq,
        rt_prim_intLt,
        rt_prim_intLe,
        rt_prim_dblAdd,
        rt_prim_dblSub,
        rt_prim_dblMul,
        rt_prim_dblDiv,
        rt_prim_dblNeg,
        rt_prim_dblSqrt,
        rt_prim_dblExp,
        rt_prim_dblLog,
        rt_prim_dblSin,
        rt_prim_dblCos,
        rt_prim_dblPow,
        rt_prim_dblEq,
        rt_prim_dblLt,
        rt_prim_dblLe,
        rt_prim_intToDbl,
        rt_prim_dblFloor,
        rt_prim_dblRound,
        rt_prim_dblTruncate,
        rt_prim_chrOrd,
        rt_prim_chrChr,
        rt_prim_chrEq,
        rt_prim_chrLt,
        rt_prim_strAppend,
        rt_prim_strLength,
        rt_prim_strEq,
        rt_prim_strCompare,
        rt_prim_strUnpack,
        rt_prim_strPack,
        rt_prim_showInt,
        rt_prim_showDouble,
        rt_prim_showChar,
        rt_prim_showString,
        rt_prim_readInt,
        rt_prim_error,
        rt_prim_putStr,
        rt_prim_getLine,
        rt_prim_exit,
    ]));
    symbols
}
/// Enter a finalized module without exposing a callable to the rest of the compiler.
fn call_entry(engine: &ExecutionEngine<'_>) -> Result<(), String> {
    // SAFETY: the verified backend emits exactly void fun_entry(void), C ABI.
    // All imports are explicitly mapped; code, constants, and root slots remain live.
    // This engine is never modified after lookup, and no pointer escapes this call.
    unsafe {
        let entry = engine
            .get_function::<unsafe extern "C" fn()>("fun_entry")
            .map_err(|e| e.to_string())?;
        entry.call();
    }
    Ok(())
}
/// Resolve declarations before MCJIT finalizes the module. Intrinsics belong to LLVM.
fn map_symbols(
    module: &inkwell::module::Module<'_>,
    engine: &ExecutionEngine<'_>,
    imports: &[(String, usize)],
) -> Result<(), String> {
    let symbols = runtime_symbols();
    for function in module
        .get_functions()
        .filter(|f| f.count_basic_blocks() == 0)
    {
        let name =
            function.get_name().to_str().map_err(|e| e.to_string())?;
        if name.starts_with("llvm.") {
            continue;
        }
        let address = symbols
            .iter()
            .find(|(symbol, _)| *symbol == name)
            .map(|(_, address)| *address)
            .or_else(|| {
                imports
                    .iter()
                    .find(|(symbol, _)| symbol == name)
                    .map(|(_, address)| *address)
            })
            .ok_or_else(|| {
                format!("unmapped JIT runtime symbol: {name}")
            })?;
        engine.add_global_mapping(&function, address);
    }
    Ok(())
}
}

인식되지 않는 임포트는 실행 전에 오류가 됩니다. 우리는 호스트 실행 파일이 심볼을 이름으로 내보내는 것에 의존하지 않습니다. 문자열 연산, 서식화, 적용, 오류, IO는 모두 22장 런타임 rlib에 도달하며, 이 rlib의 ABI는 네이티브 실행 파일이 사용하는 정적 라이브러리와 일치합니다.

코드와 루트를 함께 유지하기

세션의 소유권은 명시적입니다.

#![allow(unused)]
fn main() {
use crate::{flat, llvm::Backend};
use fun_rt::heap::GlobalRoot;
use inkwell::{
    OptimizationLevel, context::Context, execution_engine::ExecutionEngine,
};
use std::collections::BTreeMap;
type GlobalKey = (String, usize);
impl<'ctx> Jit<'ctx> {
    pub fn new(context: &'ctx Context) -> Self {
        fun_rt::rt_init();
        Self {
            engines: vec![],
            roots: BTreeMap::new(),
            queries: vec![],
            code: BTreeMap::new(),
            types: BTreeMap::new(),
            context,
        }
    }
    /// Compile a checked program, import accepted globals, then initialize only new bindings.
    /// Runtime errors retain the native runtime's process-exit semantics.
    pub fn run(&mut self, program: &flat::Program) -> Result<(), String> {
        let mut occurrences = BTreeMap::new();
        let mut keys = BTreeMap::new();
        for global in &program.globals {
            let count =
                occurrences.entry(global.name.text.clone()).or_insert(0);
            keys.insert(
                global.name.clone(),
                (global.name.text.clone(), *count),
            );
            *count += 1;
        }
        let mut checked_types = BTreeMap::new();
        for (id, shape) in &program.metadata.global_types {
            let Some(key) = keys.get(id) else { continue };
            if let Some(previous) = self.types.get(key)
                && previous != shape
            {
                return Err(format!(
                    "input changes the accepted type of {}; use :reset or :load",
                    key.0
                ));
            }
            checked_types.insert(key.clone(), shape.clone());
        }
        let inherited = keys
            .iter()
            .filter(|(_, key)| self.roots.contains_key(*key))
            .map(|(id, _)| id.clone())
            .collect();
        let backend = Backend::for_jit(self.context, program, inherited)
            .map_err(|e| e.to_string())?;
        backend.optimize(1).map_err(|e| e.to_string())?;
        let engine = backend
            .module
            .create_jit_execution_engine(OptimizationLevel::Less)
            .map_err(|e| format!("JIT setup failed: {e}"))?;
        let mut imports = vec![];
        for global in &program.globals {
            if let Some(f) = global.function
                && let Some((fast, slow, arity)) =
                    self.code.get(&keys[&global.name])
            {
                if *arity != program.funcs[f.0 as usize].params.len() {
                    return Err(format!(
                        "input changes the accepted arity of {}; use :reset or :load",
                        global.name.text
                    ));
                }
                imports.push((format!("fun.f{}", f.0), *fast));
                imports.push((format!("fun.f{}.slow", f.0), *slow));
            }
        }
        map_symbols(&backend.module, &engine, &imports)?;
        let mut pending = BTreeMap::new();
        for global in &program.globals {
            let name = &keys[&global.name];
            let slots = if let Some(slots) = self.roots.get(name) {
                slots
            } else {
                pending
                    .entry(name.clone())
                    .or_insert_with(|| GlobalRoot::new(0))
            };
            let value = backend
                .module
                .get_global(&format!("fun.g{}", global.name.name.unique));
            let Some(value) = value else { continue }; // O1 removed an unused declaration.
            engine.add_global_mapping(&value, slots.as_ptr() as usize);
        }
        let mut pending_code = BTreeMap::new();
        for global in &program.globals {
            let key = &keys[&global.name];
            if let Some(f) = global.function
                && !self.code.contains_key(key)
            {
                let fast = engine
                    .get_function_address(&format!("fun.f{}", f.0))
                    .map_err(|e| e.to_string())?;
                let slow = engine
                    .get_function_address(&format!("fun.f{}.slow", f.0))
                    .map_err(|e| e.to_string())?;
                pending_code.insert(
                    key.clone(),
                    (fast, slow, program.funcs[f.0 as usize].params.len()),
                );
            }
        }
        call_entry(&engine)?;
        // A transient entry gets a fresh slot on every input; retain even that slot
        // while its module exists, so no relocation can ever address freed storage.
        if let Some(query) =
            pending.remove(&keys[&program.metadata.main.name])
        {
            self.queries.push(query);
        }
        let query = &keys[&program.metadata.main.name];
        pending_code.remove(query);
        checked_types.remove(query);
        self.code.append(&mut pending_code);
        self.types.append(&mut checked_types);
        self.roots.append(&mut pending);
        self.engines.push(engine);
        Ok(())
    }
    /// Introspection for tests: accepted globals survive later inputs without reinitialization.
    pub fn global_word(&self, name: &str) -> Option<u64> {
        self.roots
            .iter()
            .rfind(|((text, _), _)| text == name)
            .map(|(_, root)| root.get())
    }
}
/// One explicit symbol table links the JIT to the very same ABI as native objects.
pub fn runtime_symbols() -> Vec<(&'static str, usize)> {
    macro_rules! entries {
        ($module:path => [$($name:ident),* $(,)?]) => {{
            use $module as runtime;
            vec![$((stringify!($name), runtime::$name as *const () as usize)),*]
        }};
    }
    let mut symbols = entries!(fun_rt => [
        rt_init,
        rt_alloc,
        rt_alloc_atomic,
        rt_apply,
        rt_apply1,
        rt_apply2,
        rt_apply3,
        rt_apply4,
        rt_closure_set,
        rt_make_pap,
        rt_div_zero,
        rt_error,
        rt_exit,
        rt_pattern_fail,
    ]);
    symbols.extend(entries!(fun_rt::abi => [
        rt_prim_intAdd,
        rt_prim_intSub,
        rt_prim_intMul,
        rt_prim_intNeg,
        rt_prim_intQuot,
        rt_prim_intRem,
        rt_prim_intDiv,
        rt_prim_intMod,
        rt_prim_intEq,
        rt_prim_intLt,
        rt_prim_intLe,
        rt_prim_dblAdd,
        rt_prim_dblSub,
        rt_prim_dblMul,
        rt_prim_dblDiv,
        rt_prim_dblNeg,
        rt_prim_dblSqrt,
        rt_prim_dblExp,
        rt_prim_dblLog,
        rt_prim_dblSin,
        rt_prim_dblCos,
        rt_prim_dblPow,
        rt_prim_dblEq,
        rt_prim_dblLt,
        rt_prim_dblLe,
        rt_prim_intToDbl,
        rt_prim_dblFloor,
        rt_prim_dblRound,
        rt_prim_dblTruncate,
        rt_prim_chrOrd,
        rt_prim_chrChr,
        rt_prim_chrEq,
        rt_prim_chrLt,
        rt_prim_strAppend,
        rt_prim_strLength,
        rt_prim_strEq,
        rt_prim_strCompare,
        rt_prim_strUnpack,
        rt_prim_strPack,
        rt_prim_showInt,
        rt_prim_showDouble,
        rt_prim_showChar,
        rt_prim_showString,
        rt_prim_readInt,
        rt_prim_error,
        rt_prim_putStr,
        rt_prim_getLine,
        rt_prim_exit,
    ]));
    symbols
}
/// Resolve declarations before MCJIT finalizes the module. Intrinsics belong to LLVM.
fn map_symbols(
    module: &inkwell::module::Module<'_>,
    engine: &ExecutionEngine<'_>,
    imports: &[(String, usize)],
) -> Result<(), String> {
    let symbols = runtime_symbols();
    for function in module
        .get_functions()
        .filter(|f| f.count_basic_blocks() == 0)
    {
        let name =
            function.get_name().to_str().map_err(|e| e.to_string())?;
        if name.starts_with("llvm.") {
            continue;
        }
        let address = symbols
            .iter()
            .find(|(symbol, _)| *symbol == name)
            .map(|(_, address)| *address)
            .or_else(|| {
                imports
                    .iter()
                    .find(|(symbol, _)| symbol == name)
                    .map(|(_, address)| *address)
            })
            .ok_or_else(|| {
                format!("unmapped JIT runtime symbol: {name}")
            })?;
        engine.add_global_mapping(&function, address);
    }
    Ok(())
}
/// Enter a finalized module without exposing a callable to the rest of the compiler.
fn call_entry(engine: &ExecutionEngine<'_>) -> Result<(), String> {
    // SAFETY: the verified backend emits exactly void fun_entry(void), C ABI.
    // All imports are explicitly mapped; code, constants, and root slots remain live.
    // This engine is never modified after lookup, and no pointer escapes this call.
    unsafe {
        let entry = engine
            .get_function::<unsafe extern "C" fn()>("fun_entry")
            .map_err(|e| e.to_string())?;
        entry.call();
    }
    Ok(())
}
/// Keep every engine alive until no REPL binding can reference its closures.
/// Fields drop in declaration order: release code before unregistering the slots.
pub struct Jit<'ctx> {
    engines: Vec<ExecutionEngine<'ctx>>,
    roots: BTreeMap<GlobalKey, GlobalRoot>,
    queries: Vec<GlobalRoot>,
    code: BTreeMap<GlobalKey, (usize, usize, usize)>,
    types: BTreeMap<GlobalKey, Vec<String>>,
    context: &'ctx Context,
}
}

전역 슬롯은 런타임의 GlobalRoot 소유자를 사용합니다. 그 박스 저장소는 안정된 주소에 머무르며 Boehm에 등록됩니다. UnsafeCell은 생성된 코드가 Rust의 참조 규칙과 충돌하지 않고 슬롯에 쓸 수 있게 해 줍니다. LLVM 전역을 그 주소에 매핑하면 생성된 로드와 수집기가 모두 같은 워드를 보게 됩니다. Rust 컨테이너만으로는 이 보장을 제공할 수 없습니다. Boehm은 Rust의 일반 힙을 추이적으로 스캔하지 않습니다.

소스 그룹 순서는 Prelude 전역 이름과, 같은 철자를 섀도잉하는 사용자 전역 이름을 구별합니다. 세션 로컬 리네이머 고유 번호는 한 번의 컴파일 안에서 LLVM 선언을 식별하지만, 선언을 추가하면 그 번호가 바뀔 수 있습니다. 영속적인 키로는 다른 무언가가 필요합니다. 이미 받아들여진 슬롯은 재사용되며, 그 초기화식은 건너뜁니다. 값을 가져오기 전에, 알파 정규화된 타입 지문은 재검사로 그 값의 타입이 바뀌지 않았는지 확인하고, 저장된 구문적 항수는 빠른 진입점의 인자 개수를 확인합니다. 소거 단계는 이 평탄한 메타데이터를 보존합니다. 지문 순회는 명시적인 작업 목록을 사용합니다. 질의 슬롯은 매번 새로 만들어지고 해당 엔진과 함께 유지되므로, 살아남은 재배치 중 해제된 저장 공간을 가리키는 것은 없습니다.

Jit::run은 타입이 지정된 호출 가능 객체를 가져오기 전에 모든 매핑을 마칩니다. 그 unsafe 호출은 우리가 미리 확인하는 몇 가지 사실에 의존합니다. 백엔드는 Inkwell에 요청한 C 시그니처를 정확히 그대로 내보냅니다. 모든 런타임 임포트는 해결되어 있으며, 엔진과 루트는 호출 내내 살아 있습니다. 호출 가능 객체는 즉시 사용됩니다. 우리는 그 이후로 해당 엔진에 코드를 추가하지 않습니다. Inkwell 문서에 따르면 나중에 코드를 추가하면 이전에 가져온 함수 포인터가 무효화될 수 있습니다.

#![allow(unused)]
fn main() {
use crate::{flat, llvm::Backend};
use fun_rt::heap::GlobalRoot;
use inkwell::{
    OptimizationLevel, context::Context, execution_engine::ExecutionEngine,
};
use std::collections::BTreeMap;
type GlobalKey = (String, usize);
/// Keep every engine alive until no REPL binding can reference its closures.
/// Fields drop in declaration order: release code before unregistering the slots.
pub struct Jit<'ctx> {
    engines: Vec<ExecutionEngine<'ctx>>,
    roots: BTreeMap<GlobalKey, GlobalRoot>,
    queries: Vec<GlobalRoot>,
    code: BTreeMap<GlobalKey, (usize, usize, usize)>,
    types: BTreeMap<GlobalKey, Vec<String>>,
    context: &'ctx Context,
}
impl<'ctx> Jit<'ctx> {
    pub fn new(context: &'ctx Context) -> Self {
        fun_rt::rt_init();
        Self {
            engines: vec![],
            roots: BTreeMap::new(),
            queries: vec![],
            code: BTreeMap::new(),
            types: BTreeMap::new(),
            context,
        }
    }
    /// Compile a checked program, import accepted globals, then initialize only new bindings.
    /// Runtime errors retain the native runtime's process-exit semantics.
    pub fn run(&mut self, program: &flat::Program) -> Result<(), String> {
        let mut occurrences = BTreeMap::new();
        let mut keys = BTreeMap::new();
        for global in &program.globals {
            let count =
                occurrences.entry(global.name.text.clone()).or_insert(0);
            keys.insert(
                global.name.clone(),
                (global.name.text.clone(), *count),
            );
            *count += 1;
        }
        let mut checked_types = BTreeMap::new();
        for (id, shape) in &program.metadata.global_types {
            let Some(key) = keys.get(id) else { continue };
            if let Some(previous) = self.types.get(key)
                && previous != shape
            {
                return Err(format!(
                    "input changes the accepted type of {}; use :reset or :load",
                    key.0
                ));
            }
            checked_types.insert(key.clone(), shape.clone());
        }
        let inherited = keys
            .iter()
            .filter(|(_, key)| self.roots.contains_key(*key))
            .map(|(id, _)| id.clone())
            .collect();
        let backend = Backend::for_jit(self.context, program, inherited)
            .map_err(|e| e.to_string())?;
        backend.optimize(1).map_err(|e| e.to_string())?;
        let engine = backend
            .module
            .create_jit_execution_engine(OptimizationLevel::Less)
            .map_err(|e| format!("JIT setup failed: {e}"))?;
        let mut imports = vec![];
        for global in &program.globals {
            if let Some(f) = global.function
                && let Some((fast, slow, arity)) =
                    self.code.get(&keys[&global.name])
            {
                if *arity != program.funcs[f.0 as usize].params.len() {
                    return Err(format!(
                        "input changes the accepted arity of {}; use :reset or :load",
                        global.name.text
                    ));
                }
                imports.push((format!("fun.f{}", f.0), *fast));
                imports.push((format!("fun.f{}.slow", f.0), *slow));
            }
        }
        map_symbols(&backend.module, &engine, &imports)?;
        let mut pending = BTreeMap::new();
        for global in &program.globals {
            let name = &keys[&global.name];
            let slots = if let Some(slots) = self.roots.get(name) {
                slots
            } else {
                pending
                    .entry(name.clone())
                    .or_insert_with(|| GlobalRoot::new(0))
            };
            let value = backend
                .module
                .get_global(&format!("fun.g{}", global.name.name.unique));
            let Some(value) = value else { continue }; // O1 removed an unused declaration.
            engine.add_global_mapping(&value, slots.as_ptr() as usize);
        }
        let mut pending_code = BTreeMap::new();
        for global in &program.globals {
            let key = &keys[&global.name];
            if let Some(f) = global.function
                && !self.code.contains_key(key)
            {
                let fast = engine
                    .get_function_address(&format!("fun.f{}", f.0))
                    .map_err(|e| e.to_string())?;
                let slow = engine
                    .get_function_address(&format!("fun.f{}.slow", f.0))
                    .map_err(|e| e.to_string())?;
                pending_code.insert(
                    key.clone(),
                    (fast, slow, program.funcs[f.0 as usize].params.len()),
                );
            }
        }
        call_entry(&engine)?;
        // A transient entry gets a fresh slot on every input; retain even that slot
        // while its module exists, so no relocation can ever address freed storage.
        if let Some(query) =
            pending.remove(&keys[&program.metadata.main.name])
        {
            self.queries.push(query);
        }
        let query = &keys[&program.metadata.main.name];
        pending_code.remove(query);
        checked_types.remove(query);
        self.code.append(&mut pending_code);
        self.types.append(&mut checked_types);
        self.roots.append(&mut pending);
        self.engines.push(engine);
        Ok(())
    }
    /// Introspection for tests: accepted globals survive later inputs without reinitialization.
    pub fn global_word(&self, name: &str) -> Option<u64> {
        self.roots
            .iter()
            .rfind(|((text, _), _)| text == name)
            .map(|(_, root)| root.get())
    }
}
/// One explicit symbol table links the JIT to the very same ABI as native objects.
pub fn runtime_symbols() -> Vec<(&'static str, usize)> {
    macro_rules! entries {
        ($module:path => [$($name:ident),* $(,)?]) => {{
            use $module as runtime;
            vec![$((stringify!($name), runtime::$name as *const () as usize)),*]
        }};
    }
    let mut symbols = entries!(fun_rt => [
        rt_init,
        rt_alloc,
        rt_alloc_atomic,
        rt_apply,
        rt_apply1,
        rt_apply2,
        rt_apply3,
        rt_apply4,
        rt_closure_set,
        rt_make_pap,
        rt_div_zero,
        rt_error,
        rt_exit,
        rt_pattern_fail,
    ]);
    symbols.extend(entries!(fun_rt::abi => [
        rt_prim_intAdd,
        rt_prim_intSub,
        rt_prim_intMul,
        rt_prim_intNeg,
        rt_prim_intQuot,
        rt_prim_intRem,
        rt_prim_intDiv,
        rt_prim_intMod,
        rt_prim_intEq,
        rt_prim_intLt,
        rt_prim_intLe,
        rt_prim_dblAdd,
        rt_prim_dblSub,
        rt_prim_dblMul,
        rt_prim_dblDiv,
        rt_prim_dblNeg,
        rt_prim_dblSqrt,
        rt_prim_dblExp,
        rt_prim_dblLog,
        rt_prim_dblSin,
        rt_prim_dblCos,
        rt_prim_dblPow,
        rt_prim_dblEq,
        rt_prim_dblLt,
        rt_prim_dblLe,
        rt_prim_intToDbl,
        rt_prim_dblFloor,
        rt_prim_dblRound,
        rt_prim_dblTruncate,
        rt_prim_chrOrd,
        rt_prim_chrChr,
        rt_prim_chrEq,
        rt_prim_chrLt,
        rt_prim_strAppend,
        rt_prim_strLength,
        rt_prim_strEq,
        rt_prim_strCompare,
        rt_prim_strUnpack,
        rt_prim_strPack,
        rt_prim_showInt,
        rt_prim_showDouble,
        rt_prim_showChar,
        rt_prim_showString,
        rt_prim_readInt,
        rt_prim_error,
        rt_prim_putStr,
        rt_prim_getLine,
        rt_prim_exit,
    ]));
    symbols
}
/// Resolve declarations before MCJIT finalizes the module. Intrinsics belong to LLVM.
fn map_symbols(
    module: &inkwell::module::Module<'_>,
    engine: &ExecutionEngine<'_>,
    imports: &[(String, usize)],
) -> Result<(), String> {
    let symbols = runtime_symbols();
    for function in module
        .get_functions()
        .filter(|f| f.count_basic_blocks() == 0)
    {
        let name =
            function.get_name().to_str().map_err(|e| e.to_string())?;
        if name.starts_with("llvm.") {
            continue;
        }
        let address = symbols
            .iter()
            .find(|(symbol, _)| *symbol == name)
            .map(|(_, address)| *address)
            .or_else(|| {
                imports
                    .iter()
                    .find(|(symbol, _)| symbol == name)
                    .map(|(_, address)| *address)
            })
            .ok_or_else(|| {
                format!("unmapped JIT runtime symbol: {name}")
            })?;
        engine.add_global_mapping(&function, address);
    }
    Ok(())
}
/// Enter a finalized module without exposing a callable to the rest of the compiler.
fn call_entry(engine: &ExecutionEngine<'_>) -> Result<(), String> {
    // SAFETY: the verified backend emits exactly void fun_entry(void), C ABI.
    // All imports are explicitly mapped; code, constants, and root slots remain live.
    // This engine is never modified after lookup, and no pointer escapes this call.
    unsafe {
        let entry = engine
            .get_function::<unsafe extern "C" fn()>("fun_entry")
            .map_err(|e| e.to_string())?;
        entry.call();
    }
    Ok(())
}
}

성공한 뒤에 정의 확정하기

Repl::line은 루프 안에서 명령을 디스패치합니다. 선언은 후보 소스 목록에 추가되고, 비공개 $repl 진입점으로 검사되며, 그 목록이 수락된 목록을 대체하기 전에 초기화됩니다. 이 비공개 이름은 Fun으로 작성할 수 없으므로 로드된 main과 충돌하지 않습니다. 로드도 마찬가지로 대체할 세션을 먼저 구성한 뒤에 교체합니다.

식 실행은 평범한 언어 연산을 사용합니다:

#![allow(unused)]
fn main() {
use crate::{
    ast, core,
    jit::Jit,
    session::{Options, Session},
    typecheck::TypedProgram,
};
use inkwell::context::Context;
use rustyline::{DefaultEditor, error::ReadlineError};
use std::io::{self, IsTerminal, Write};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// The JIT is the interactive default; interpreters remain useful for comparison.
pub enum Engine {
    Jit,
    Core,
    Anf,
    Flat,
}
#[derive(Clone)]
struct Input {
    name: String,
    source: String,
}
/// Source definitions and executable definitions commit together after a successful input.
pub struct Repl<'ctx> {
    inputs: Vec<Input>,
    jit: Jit<'ctx>,
    context: &'ctx Context,
    engine: Engine,
    lint: bool,
    fuel: usize,
}
impl<'ctx> Repl<'ctx> {
    pub fn new(context: &'ctx Context, lint: bool, fuel: usize) -> Self {
        Self {
            inputs: vec![],
            jit: Jit::new(context),
            context,
            engine: Engine::Jit,
            lint,
            fuel,
        }
    }
    fn reset(&mut self) {
        self.jit = Jit::new(self.context);
        self.inputs.clear();
    }
    /// Process one input. False requests a clean exit; diagnostics leave the session usable.
    pub fn line(&mut self, line: &str) -> Result<bool, String> {
        let line = line.trim();
        if line.is_empty() {
            return Ok(true);
        }
        let (command, rest) =
            line.split_once(char::is_whitespace).unwrap_or((line, ""));
        let rest = rest.trim();
        match command {
            ":q" | ":quit" => return Ok(false),
            ":reset" => {
                self.reset();
                println!("Reset.");
            }
            ":engine" => {
                if rest.is_empty() {
                    println!(
                        "{}",
                        format!("{:?}", self.engine).to_lowercase()
                    );
                } else {
                    let engine = match rest {
                        "jit" => Engine::Jit,
                        "core" => Engine::Core,
                        "anf" => Engine::Anf,
                        "flat" => Engine::Flat,
                        _ => {
                            return Err(
                                "expected :engine jit|core|anf|flat".into(),
                            );
                        }
                    };
                    // Switching engines rebuilds pure globals; source definitions survive.
                    self.jit = Jit::new(self.context);
                    self.engine = engine;
                    println!("Engine: {rest}");
                }
            }
            ":load" => {
                if rest.is_empty() {
                    return Err("expected :load FILE".into());
                }
                let input = Input {
                    name: rest.into(),
                    source: std::fs::read_to_string(rest)
                        .map_err(|e| e.to_string())?,
                };
                // Keep the old session until both checking and JIT preparation succeed.
                let mut next =
                    Self::new(self.context, self.lint, self.fuel);
                next.engine = self.engine;
                next.evaluate(std::slice::from_ref(&input), "pure ()")?;
                next.inputs.push(input);
                *self = next;
                println!("Loaded {rest}");
            }
            ":t" | ":type" => {
                let (_, typed) = prepare(&self.inputs, rest, false)?;
                println!("{rest} : {}", typed.main_description());
            }
            ":k" => {
                let (mut sess, typed) = prepare(&self.inputs, "()", false)?;
                let file = sess
                    .sources
                    .add("<kind>", format!("type ReplKind = {rest}"));
                let syntax = crate::parse::parse(&mut sess, file)
                    .map_err(|_| diagnostics(&sess))?;
                let alias =
                    syntax.aliases.first().ok_or("expected :k TYPE")?;
                let mut store = crate::kinds::Store::default();
                let expanded = crate::kinds::expand(
                    &alias.body,
                    &typed.surface.data.aliases,
                    alias.span,
                )
                .map_err(|e| e.message)?;
                let kind = store
                    .infer(
                        &expanded,
                        &typed.surface.data.kinds,
                        &mut std::collections::BTreeMap::new(),
                        false,
                        alias.span,
                    )
                    .map_err(|e| e.message)?;
                println!("{rest} : {}", store.finish(&kind));
            }
            ":core" | ":anf" | ":llvm" => self.dump(rest, command)?,
            ":help" => println!(
                "EXPR; declarations; :t EXPR; :k TYPE; :core/:anf/:llvm EXPR; :load FILE; :reset; :engine jit|core|anf|flat; :quit"
            ),
            _ if command.starts_with(':') => {
                return Err(format!(
                    "unknown command: {command}; use :help"
                ));
            }
            _ => {
                // Trial parsing distinguishes declarations from expressions without publishing diagnostics.
                let mut trial = Session::default();
                let file = trial.sources.add("<input>", line.into());
                if crate::parse::parse(&mut trial, file).is_ok() {
                    let mut candidate = self.inputs.clone();
                    candidate.push(Input {
                        name: format!("<input {}>", candidate.len() + 1),
                        source: line.into(),
                    });
                    self.evaluate(&candidate, "pure ()")?;
                    self.inputs = candidate;
                } else {
                    let inputs = self.inputs.clone();
                    let (_, typed) = prepare(&inputs, line, false)?;
                    let query = action_query(line, &typed.main_type);
                    self.evaluate(&inputs, &query)?;
                }
            }
        }
        Ok(true)
    }
    fn evaluate(
        &mut self,
        inputs: &[Input],
        query: &str,
    ) -> Result<(), String> {
        let (mut sess, typed) = prepare(inputs, query, true)?;
        let core = lower_core(&mut sess, typed, self.lint)?;
        if self.engine == Engine::Core {
            return crate::eval::run_io(&core, self.fuel)
                .map_err(|e| e.to_string());
        }
        let anf = lower_anf(&mut sess, core, self.lint)?;
        if self.engine == Engine::Anf {
            return crate::anf_machine::run(&anf, self.fuel, true)
                .map(|_| ())
                .map_err(|e| e.to_string());
        }
        let flat = crate::closure::convert(&mut sess, anf);
        crate::flat_lint::lint(&flat)?;
        if self.engine == Engine::Jit {
            match self.jit.run(&flat) {
                Ok(()) => return Ok(()),
                Err(e) if e.starts_with("JIT setup failed:") => {
                    eprintln!("fun: {e}; using flat interpreter");
                    self.engine = Engine::Flat;
                }
                Err(e) => return Err(e),
            }
        }
        crate::flat_machine::run(&flat, self.fuel, true)
            .map(|_| ())
            .map_err(|e| e.to_string())
    }
    fn dump(&self, expression: &str, pass: &str) -> Result<(), String> {
        let (mut sess, mut typed) =
            prepare(&self.inputs, expression, false)?;
        if pass == ":llvm" {
            let query = action_query(expression, &typed.main_type);
            (sess, typed) = prepare(&self.inputs, &query, true)?;
        }
        let core = lower_core(&mut sess, typed, self.lint)?;
        if pass == ":core" {
            for group in &core.groups {
                println!("{group}");
            }
            return Ok(());
        }
        let anf = lower_anf(&mut sess, core, self.lint)?;
        if pass == ":anf" {
            for (name, body) in &anf.globals {
                println!("{name} = {body}");
            }
            return Ok(());
        }
        let flat = crate::closure::convert(&mut sess, anf);
        let backend = crate::llvm::Backend::for_jit(
            self.context,
            &flat,
            Default::default(),
        )
        .map_err(|e| e.to_string())?;
        print!("{}", backend.ir());
        Ok(())
    }
}
fn diagnostics(sess: &Session) -> String {
    let mut bytes = vec![];
    for diagnostic in &sess.diagnostics {
        let _ = diagnostic.render(&sess.sources, false, &mut bytes);
    }
    String::from_utf8_lossy(&bytes).trim_end().into()
}
fn append(to: &mut ast::Program, mut from: ast::Program) {
    to.bindings.append(&mut from.bindings);
    to.fixities.append(&mut from.fixities);
    to.data.append(&mut from.data);
    to.aliases.append(&mut from.aliases);
    to.classes.append(&mut from.classes);
    to.instances.append(&mut from.instances);
}
fn prepare(
    inputs: &[Input],
    query: &str,
    io: bool,
) -> Result<(Session, TypedProgram), String> {
    let mut sess = Session {
        options: Options {
            require_io: io,
            ..Options::default()
        },
        entry_name: Some("$repl".into()),
        ..Session::default()
    };
    let mut program = None;
    let mut later_files = vec![];
    for (index, input) in inputs.iter().enumerate() {
        let file =
            sess.sources.add(input.name.clone(), input.source.clone());
        if index > 0 {
            later_files.push(file);
        }
        let parsed = crate::parse::parse(&mut sess, file)
            .map_err(|_| diagnostics(&sess))?;
        if index > 0
            && parsed.fixities.iter().any(|f| {
                f.operators.iter().any(|op| {
                    !parsed.bindings.iter().any(|b| b.name == *op)
                })
            })
        {
            return Err("a REPL fixity must accompany its operator definition in the same input".into());
        }
        if let Some(program) = &mut program {
            append(program, parsed);
        } else {
            program = Some(parsed);
        }
    }
    let file = sess.sources.add("<query>", format!("main = {query}"));
    let mut entry = crate::parse::parse(&mut sess, file)
        .map_err(|_| diagnostics(&sess))?;
    entry.bindings[0].name = "$repl".into();
    let program = if let Some(mut program) = program {
        append(&mut program, entry);
        program
    } else {
        entry
    };
    let typed = crate::typecheck::check(&mut sess, program)
        .map_err(|_| diagnostics(&sess))?;
    // Retained code and replaying interpreters must agree on name resolution.
    // Put Prelude overrides in the initial declaration block or a loaded file.
    let prelude_names = typed
        .surface
        .groups
        .iter()
        .flat_map(|g| &g.binds)
        .filter(|b| Some(b.span.file) == sess.prelude_file)
        .map(|b| b.id.text.as_str())
        .collect::<std::collections::BTreeSet<_>>();
    for binding in typed.surface.groups.iter().flat_map(|g| &g.binds) {
        if later_files.contains(&binding.span.file)
            && prelude_names.contains(binding.id.text.as_str())
        {
            return Err(format!(
                "{} shadows the Prelude after earlier definitions; use :reset or :load",
                binding.id.text
            ));
        }
    }
    Ok((sess, typed))
}
fn lower_core(
    sess: &mut Session,
    typed: TypedProgram,
    lint: bool,
) -> Result<core::Program, String> {
    let core = crate::elaborate::elaborate(sess, typed)
        .map_err(|_| diagnostics(sess))?;
    if lint {
        core::lint(&core)?;
    }
    let core = crate::match_compile::compile(sess, core)
        .map_err(|_| diagnostics(sess))?;
    if lint {
        core::lint(&core)?;
    }
    Ok(core)
}
fn lower_anf(
    sess: &mut Session,
    core: core::Program,
    lint: bool,
) -> Result<crate::anf::Program, String> {
    let mut anf = crate::anf::convert(sess, crate::erase::erase(core));
    crate::anf_simplify::simplify(sess, &mut anf);
    if lint {
        crate::anf_lint::lint(&anf)?;
    }
    Ok(anf)
}
/// Terminal input uses rustyline; redirected input has deterministic, prompt-free output.
pub fn run(
    lint: bool,
    fuel: usize,
) -> Result<(), Box<dyn std::error::Error>> {
    let context = Context::create();
    let mut repl = Repl::new(&context, lint, fuel);
    let interactive = io::stdin().is_terminal();
    let mut editor = if interactive {
        Some(DefaultEditor::new()?)
    } else {
        None
    };
    if interactive {
        println!("Fun: JIT ready on first input. Type :help for commands.");
    }
    loop {
        let line = if let Some(editor) = &mut editor {
            match editor.readline("fun> ") {
                Ok(line) => {
                    editor.add_history_entry(&line)?;
                    line
                }
                Err(ReadlineError::Eof | ReadlineError::Interrupted) => {
                    break;
                }
                Err(e) => return Err(e.into()),
            }
        } else {
            let mut line = String::new();
            if io::stdin().read_line(&mut line)? == 0 {
                break;
            }
            line
        };
        match repl.line(&line) {
            Ok(false) => break,
            Ok(true) => {}
            Err(e) => eprintln!("fun: {e}"),
        }
        io::stdout().flush()?;
    }
    Ok(())
}
/// Both display and IO sequencing are ordinary, type-checked Prelude operations.
fn action_query(expression: &str, ty: &crate::types::Type) -> String {
    if matches!(ty, crate::types::Type::Con(id, _) if id.name == "IO") {
        format!("({expression}) >> pure ()")
    } else {
        format!("print ({expression})")
    }
}
}

그렇게 만들어진 질의는 다시 추론과 정교화를 거칩니다. 래퍼는 클래스 해결이나 IO 추상화를 우회하지 않습니다. 타입, 카인드, IR 명령은 실행 전에 멈춥니다. 새로 추가된 순회와 명령 제어는 모두 루프를 사용합니다. 더 큰 Rust 스택이나 스택 확장 라이브러리는 도입하지 않습니다. 앞선 장들에서 물려받은 재귀적인 프런트엔드 패스는 기존의 한계를 그대로 유지합니다.

우리는 입력마다 수락된 소스를 재검사하고 후보 모듈을 로워링합니다. 기존 전역 값과 빠른/느린 함수 진입점은 임포트되므로, 초기화 상태와 코드 동일성이 모두 보존됩니다. 사용되지 않는 재생성된 로컬 본문은 최적화로 제거되지만, 컴파일 작업량과 유지되는 기계어 코드는 세션이 진행될수록 늘어납니다. 이렇게 하면 소유권을 살펴보기 쉬워집니다. 증분 검사와 코드 회수를 구현하려면 의존성과 수명을 다루는 장치가 추가로 필요합니다.

참고. 런타임 오류와 exitWith는 JIT REPL에서도 네이티브 프로세스 종료 의미론을 그대로 유지합니다. 우리는 생성된 프레임을 가로질러 되감거나 점프하지 않습니다. 컴파일러 오류는 복구할 수 있습니다. 실행 엔진 생성에 실패하면 REPL은 Flat을 선택합니다. 검증기 실패는 폴백으로 감추지 않고 보고합니다. run --engine=jit를 명시적으로 지정하면 설정 실패를 보고합니다.

실행해 보기

21장의 LLVM 22 및 Boehm 설정으로 빌드하십시오:

$ python3 scripts/build_runtime.py 22
$ cargo build -p ch22_jit
$ target/debug/fun repl --lint
Fun: JIT ready on first input. Type :help for commands.
fun> make x = \y -> x + y
fun> saved = make 40
fun> saved 2
42
fun> :t saved
saved : Int -> Int
fun> :k Maybe
Maybe : * -> *
fun> :reset
Reset.
fun> :quit
$ target/debug/fun run crates/ch22_jit/tests/programs/native/acceptance.fun --engine=jit --lint
5.0
3.0
6.0
[3.0,10.0]
True
50000005000000
$ target/debug/fun dump --pass=llvm crates/ch22_jit/tests/programs/jit/returned-function.fun > build/jit-example.ll

fun과 fun-ch22는 하나의 드라이버를 공유합니다. CLI는 기본 run 엔진으로 Flat을 유지합니다. REPL의 기본값은 JIT입니다. --fuel과 테스트 전용 --print-value는 계속 인터프리터 기능으로 남습니다. REPL 입력을 리디렉션하면 프롬프트와 배너를 생략하므로 대화 기록을 바이트 단위로 비교할 수 있습니다.

JIT는 인터프리터나 독립 실행 파일과 같은 답을 내야 합니다. 또한 긴 알려진 호출(known call) 재귀와 제한된 힙에서의 할당도 견뎌야 합니다. 알려진 호출은 21장의 꼬리 호출 보장을 유지합니다. C apply를 거치는 호출에는 그 장에서 논의한 한계가 여전히 있습니다.

클로저를 정의하고, 정의를 더 입력한 다음, 예전 클로저를 다시 호출해 보십시오. 수명 테스트는 모듈 사이에서 컬렉션을 강제로 일으켜 이를 더 밀어붙입니다. 값이 스코프에 남아 있는 것만으로는 충분하지 않습니다. 캡처된 데이터와 그 데이터가 가리키는 기계어 코드가 모두 여전히 존재해야 합니다.

연습 문제

  1. 준비 운동. 두 입력에 걸쳐 saved = make 40의 소유자를 추적하십시오. 어느 소유자가 환경을 보호하고 어느 소유자가 코드 주소를 보호하는지 밝히십시오.
  2. 중급. 런타임 매핑 하나를 제거하고 실패를 살펴보십시오. 생성된 진입점을 호출하기 전에 그 실패를 감지해야 하는 이유를 설명하십시오.
  3. 중급. 사용자 정의가 Prelude 이름을 가리는 회귀 테스트를 추가하십시오. 철자만으로는 그 영속 슬롯을 식별할 수 없는 이유를 보이십시오.
  4. 고급. 수락된 정의를 유지하면서 런타임 오류에서 복구하는 격리된 워커 프로토콜을 설계하십시오. 무엇을 재실행해야 하는지 명시하십시오.
  5. 심화. 모듈 회수 스킴을 제안하십시오. 생성자 안의 클로저, 부분 적용, 전역 값, 캡처된 환경을 고려하십시오.

더 읽을거리

23장: 최적화

새 파일

  • src/rep.rs
  • src/reach.rs
  • src/dump.rs

변경된 파일

  • src/erase.rs
  • src/llvm/values.rs
  • src/llvm/primitives.rs
  • src/llvm/control.rs
  • src/llvm/mod.rs
  • src/anf.rs
  • src/anf_simplify.rs
  • src/anf_lint.rs
  • src/closure.rs
  • src/flat.rs
  • src/flat_lint.rs
  • src/primitives.rs
  • rt/src/primitives.rs
  • rt/src/abi.rs
  • src/main.rs
  • src/repl.rs
  • src/jit.rs
  • src/lib.rs
  • rt/src/lib.rs

동기

올바른 답을 얻는 것은 최적화기에게 좋은 출발점입니다. 우리는 최적화기가 같은 계산을 두 번 하거나 산술 명령어 사이사이마다 새 박스를 할당하는 일도 피하기를 바랍니다. 더 빨라 보이는 식이 알고 보면 같은 결과를 얻는 꽤 비싼 방법일 수도 있습니다.

우리는 몇 가지 목표가 분명한 개선을 하고, 그 개선이 실제로 무엇을 바꾸는지 살펴보겠습니다. 트레이스와 머신 스텝은 중복되거나 순서가 바뀐 작업을 잡아내는 데 도움이 됩니다. 의존성 그래프는 어떤 전역 정의가 여전히 필요한지 알려 줍니다. 타입 정보는 코드 생성이 중간값을 반복해서 박싱하는 대신 네이티브 산술을 사용할 수 있는 때를 알려 줍니다.

이것들은 작은 최적화이지만, 더 큰 최적화기도 답해야 하는 질문들을 드러냅니다. 이 식에 대해 우리는 무엇을 알고 있습니까? 이 재작성은 무엇을 절약합니까? 그리고 평가 순서는 어떻게 됩니까?

현재의 언어

새로 추가된 소스 함수는 trace :: String -> a -> a 하나뿐입니다. 이 함수는 첫 번째 인자와 개행 문자를 stderr에 쓴 다음, 두 번째 인자를 그대로 반환합니다. 인자는 여전히 엄격하게 평가됩니다. 두 번째 인자를 평가하는 도중 발생한 오류는 트레이스가 출력되기 전에 일어납니다. trace는 진단 기능이며, 일반적인 프로그램 효과를 위한 IO의 대안이 아닙니다.

-- stderr: exact
f x y = x + y

main = print (f (trace "left" 20) (trace "right" 22))

이 코드는 stderr에 left와 그 뒤에 right를 출력하고, stdout에 42를 출력합니다. 이제 어떤 호출을 다른 인자 너머로 옮기면 그 변화가 눈에 보이게 됩니다. 런타임은 트레이스를 쓰기 전에 stdout을 플러시하여 앞선 출력의 순서를 보존합니다.

이론

작업 보존하기

엄격한 언어에서 let x = f y in x + x는 f y를 한 번 평가합니다. x의 두 출현 모두를 f y로 치환하면 그것을 두 번 평가하게 됩니다. 이는 비용을 바꾸고 트레이스를 반복할 수도 있습니다. 실패하는 식 역시 관찰 가능합니다. 사용되지 않는 0으로 나누기도 그냥 사라질 수는 없습니다.

우리의 단순화기는 아톰을 읽는 데 아무 작업도 들지 않으므로 아톰을 치환할 수 있습니다. 사용되지 않는 전역(total) 계산은 제거할 수 있습니다. 즉시 적용되는 람다 본문을 유일한 호출 지점으로 옮길 수 있으며, 이때 이미 평가된 인자는 보존합니다. 두 번 호출되는 람다에는 그 규칙을 사용할 수 없습니다. 이러한 제약 덕분에 전역적인 비용 분석 없이도 이 작은 변환을 이해할 수 있습니다 (Appel 1992).

ANF 머신에 단순화를 켰을 때와 껐을 때의 스텝 수를 세도록 요청할 수 있습니다. 결과, 오류, 트레이스는 일치해야 하며, 단순화된 프로그램이 더 많은 스텝을 거쳐서는 안 됩니다. 이를 통해 실수로 작업을 중복시키는 재작성을 잡아내는 유용한 방법을 얻게 됩니다. 이것이 실제 소요 시간의 단축을 증명하지는 않지만, 특정 CPU에 의존하지 않고 실제로 측정할 수 있는 주장을 검증해 줍니다.

도달 가능한 전역 정의 유지하기

전역 참조는 의존성 그래프의 간선이 됩니다. main에서 시작하여 작업 목록(worklist)이 이 간선들을 따라가며, 새로 도달한 전역 정의마다 선행자를 하나씩 기록해 둡니다. 이는 9장의 의존성 분석과 비슷하지만, 다른 질문에 답합니다. 이 실행에 필요할 수 있는 정의는 무엇입니까?

엄격한 초기화는 루트를 추가합니다. 사용되지 않는 초기화식도 main보다 먼저 트레이스를 출력하거나, 실패하거나, 발산할 수 있습니다. 우리는 지역 분류기가 무해함을 입증할 수 없는 초기화식은 모두 유지합니다. 호출과 제어 흐름 초기화식은 계속 루트로 남습니다. 클로저를 할당해도 그 본문은 실행되지 않으므로, 사용되지 않는 함수는 제거할 수 있습니다. 할당 실패는 소스 비용 모델의 범위 밖입니다.

살아남은 전역 정의들은 원래의 의존성 순서를 그대로 유지합니다. REPL에서는 이후 입력이 보이는 바인딩 중 어느 것이든 참조할 수 있으므로, 보이는 모든 바인딩이 루트가 됩니다. 증거(witness) 덤프는 유지된 각 바인딩에 대해 그것을 발견한 간선을 하나씩 보여 주므로, 가지치기 결정을 직접 들여다볼 수 있습니다.

타입 증거는 유지하고, 저장 방식은 지역적으로 선택하기

소거된 식은 더 이상 System F 타입을 지니지 않지만, 컴파일러는 그 타입이 확립한 작은 사실 하나를 유지할 수 있습니다.

#![allow(unused)]
fn main() {
impl std::fmt::Display for Rep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Int => "Int",
            Self::Double => "Double",
            Self::Word => "Word",
        })
    }
}
use crate::{
    anf::{Anf, Atom, Rhs, Var},
    core,
    names::Id,
};
use std::collections::BTreeMap;
pub type Table = BTreeMap<Id, Rep>;
/// Missing evidence is an internal error, never an invitation to infer a storage tag.
pub fn get(table: &Table, id: &Id) -> Result<Rep, String> {
    table
        .get(id)
        .copied()
        .ok_or_else(|| format!("missing representation evidence for {id}"))
}
pub fn atom(table: &Table, a: &Atom) -> Result<Rep, String> {
    match a {
        Atom::Var(v) | Atom::Global(v) => get(table, v),
        Atom::Lit(l) => Ok(crate::erase::literal_rep(l)),
    }
}
/// Numeric operands are checked against the primitive's declared signature.
pub fn primitive(
    table: &Table,
    op: crate::ast::Op,
    args: &[Atom],
    result: &Var,
    metadata: &core::Program,
) -> Result<(), String> {
    use crate::primitives::Prim;
    let generic = matches!(
        op,
        crate::ast::Op::Runtime(Prim::Trace | Prim::Error | Prim::Exit)
    );
    let types = if generic {
        vec![core::Type::Var(0)]
    } else {
        vec![]
    };
    let (mut ty, _) = crate::primitives::core_signature(
        op,
        &types,
        &metadata.data_types,
    )?;
    for arg in args {
        let core::Type::Arrow(a, b) = ty else {
            return Err("primitive arity evidence".into());
        };
        if !matches!(a.as_ref(), core::Type::Var(_))
            && atom(table, arg)? != crate::erase::representation(&a)
        {
            return Err(format!(
                "contradictory operand evidence for {}: {:?}",
                op.text(),
                arg
            ));
        }
        ty = *b;
    }
    let expected = if matches!(op, crate::ast::Op::Runtime(Prim::Trace)) {
        atom(table, &args[1])?
    } else if matches!(ty, core::Type::Var(_)) {
        get(table, result)?
    } else {
        crate::erase::representation(&ty)
    };
    if get(table, result)? != expected {
        return Err(format!("contradictory result evidence for {result}"));
    }
    Ok(())
}
/// Copy evidence to the beta continuation's fresh result. No type classification occurs here.
pub fn beta_results(program: &mut crate::anf::Program) {
    let mut todo = program
        .globals
        .iter()
        .map(|(_, b)| b.as_ref())
        .collect::<Vec<_>>();
    let mut joins = BTreeMap::new();
    let mut pending = vec![];
    while let Some(b) = todo.pop() {
        match b {
            Anf::Let(v, r, rest) => {
                if !program.reps.contains_key(v) {
                    let Anf::Jump(j, _) = rest.as_ref() else {
                        panic!("beta result continuation")
                    };
                    pending.push((v.clone(), j.clone()));
                }
                if let Rhs::Lambda(l) = r {
                    todo.push(&l.body);
                }
                todo.push(rest);
            }
            Anf::LetRec(bs, r) => {
                todo.push(r);
                for (_, l) in bs {
                    todo.push(&l.body);
                }
            }
            Anf::Join(j, ps, b, r) => {
                if ps.len() == 1 {
                    joins.insert(j.clone(), program.reps[&ps[0]]);
                }
                todo.push(b);
                todo.push(r);
            }
            Anf::Case(_, alts) => {
                for (_, _, b) in alts {
                    todo.push(b);
                }
            }
            _ => {}
        }
    }
    for (v, j) in pending {
        program.reps.insert(v, joins[&j]);
    }
}
pub fn lint_anf(program: &crate::anf::Program) -> Result<(), String> {
    let mut todo = vec![];
    for (v, b) in &program.globals {
        get(&program.reps, v)?;
        todo.push(b.as_ref());
    }
    while let Some(b) = todo.pop() {
        match b {
            Anf::Let(v, r, rest) => {
                get(&program.reps, v)?;
                match r {
                    Rhs::Prim(p, args, _) => primitive(
                        &program.reps,
                        *p,
                        args,
                        v,
                        &program.metadata,
                    )?,
                    Rhs::Lambda(l) => todo.push(&l.body),
                    _ => {}
                }
                todo.push(rest);
            }
            Anf::LetRec(bs, r) => {
                todo.push(r);
                for (_, l) in bs {
                    todo.push(&l.body);
                }
            }
            Anf::Join(_, _, b, r) => {
                todo.push(b);
                todo.push(r);
            }
            Anf::Case(_, alts) => {
                for (_, _, b) in alts {
                    todo.push(b);
                }
            }
            _ => {}
        }
    }
    Ok(())
}
pub fn lint_flat(program: &crate::flat::Program) -> Result<(), String> {
    use crate::flat::{Flat, Rhs};
    let mut todo = program
        .funcs
        .iter()
        .map(|f| f.body.as_ref())
        .collect::<Vec<_>>();
    for g in &program.globals {
        get(&program.reps, &g.name)?;
        todo.push(g.body.as_ref());
    }
    while let Some(b) = todo.pop() {
        match b {
            Flat::Let(v, r, rest) => {
                get(&program.reps, v)?;
                if let Rhs::Prim(p, args, _) = r {
                    primitive(
                        &program.reps,
                        *p,
                        args,
                        v,
                        &program.metadata,
                    )?;
                }
                todo.push(rest);
            }
            Flat::Join(_, _, b, r) => {
                todo.push(b);
                todo.push(r);
            }
            Flat::Case(_, alts) => {
                for (_, _, b) in alts {
                    todo.push(b);
                }
            }
            _ => {}
        }
    }
    Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Rep {
    Int,
    Double,
    Word,
}
}

Int와 Double은 수치 타입임을 확립합니다. Word는 타입 변수를 포함한 나머지 모두를 포괄합니다. 이것은 증거이지 저장 레이아웃이 아닙니다. 호출이 반환한 Int는 여전히 태그가 붙은 워드로 도착합니다. 그것을 언제 레지스터로 디코딩할지는 코드 생성이 결정합니다. 호출, 반환, 필드, 캡처, 전역 정의, 조인 매개변수는 20장의 균일한 ABI를 그대로 유지합니다 (Leroy 1990).

정수 레지스터 연산에는 한 단계가 더 필요합니다. LLVM의 i64는 64비트이지만, Fun의 부호 있는 정수는 63비트입니다. 을 자기 자신과 더하면 가 나와야 합니다. 정규화를 하지 않으면 뒤따르는 비교는 양수를 보게 되고, 2로 나누면 부호가 잘못된 결과가 나옵니다. 산술 연산 후에 우리는 왼쪽으로 한 번 시프트한 다음, 오른쪽으로 한 번 산술 시프트합니다. 이렇게 하면 62번 비트가 부호 확장되어, 이후에 사용되기 전에 Fun의 범위가 복원됩니다. 반면 를 자기 자신과 더하면 음수가 아니라 0으로 순환합니다.

Double 산술은 중간 결과를 부동소수점 레지스터에 둘 수 있습니다. 우리의 호출 규약은 워드를 전달하므로, 결과를 다른 함수에 넘길 때는 여전히 박스가 필요합니다. 그래서 기본 연산으로 된 Double 누적 루프는 꼬리 호출 지점에서 반복마다 한 번씩 누산기를 여전히 박싱합니다. 개선된 점은 그 과정에서 생기는 산술 중간값을 하나하나 박싱하지는 않는다는 것입니다.

할당 횟수를 세어 이 차이를 확인하겠습니다. 오버로드된 소스 연산자는 여전히 사전 호출을 할 수 있으므로, 이런 직접적인 원시 루프를 만들어 내지 못할 수도 있습니다. 그런 사전을 특수화하는 것은 별개의 최적화 기회입니다.

구현

트레이스 동작은 공유 런타임이 담당합니다. 두 인터프리터와 네이티브 ABI가 모두 이를 사용하므로, 임의의 클로저조차 반환값이 될 수 있습니다:

#![allow(unused)]
fn main() {
use crate::layout::normalize;
#[derive(Clone, Debug, PartialEq)]
/// Only primitive input/output types: arbitrary algebraic values stay in each engine.
pub enum Value {
    Int(i64),
    Double(f64),
    Char(char),
    String(String),
    Bool(bool),
    Chars(Vec<char>),
    MaybeInt(Option<i64>),
    Unit,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Failure {
    Runtime(String),
    Exit(i32),
}
impl From<String> for Failure {
    fn from(s: String) -> Self {
        Self::Runtime(s)
    }
}
impl From<&str> for Failure {
    fn from(s: &str) -> Self {
        Self::Runtime(s.into())
    }
}
impl std::fmt::Display for Failure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Runtime(s) => f.write_str(s),
            Self::Exit(n) => write!(f, "exit {n}"),
        }
    }
}
/// Optional surrounding whitespace and sign; decimal digits only, within signed 63 bits.
pub fn read_int(source: &str) -> Option<i64> {
    let s = source.trim();
    let digits = s.strip_prefix(['+', '-']).unwrap_or(s);
    if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
        return None;
    }
    s.parse().ok().filter(|n| {
        (crate::layout::INT_MIN..=crate::layout::INT_MAX).contains(n)
    })
}
macro_rules! primitives {($($variant:ident => $name:literal),* $(,)?)=>{
    #[derive(Clone,Copy,Debug,PartialEq,Eq)] pub enum Prim {$($variant),*}
    pub const ALL:&[Prim]=&[$(Prim::$variant),*];
    impl Prim {pub fn name(self)->&'static str {match self {$(Self::$variant=>$name),*}}}
};}
primitives! {
    IntAdd=>"prim_intAdd",IntSub=>"prim_intSub",IntMul=>"prim_intMul",IntNeg=>"prim_intNeg",IntQuot=>"prim_intQuot",IntRem=>"prim_intRem",IntDiv=>"prim_intDiv",IntMod=>"prim_intMod",IntEq=>"prim_intEq",IntLt=>"prim_intLt",IntLe=>"prim_intLe",
    DblAdd=>"prim_dblAdd",DblSub=>"prim_dblSub",DblMul=>"prim_dblMul",DblDiv=>"prim_dblDiv",DblNeg=>"prim_dblNeg",DblSqrt=>"prim_dblSqrt",DblExp=>"prim_dblExp",DblLog=>"prim_dblLog",DblSin=>"prim_dblSin",DblCos=>"prim_dblCos",DblPow=>"prim_dblPow",DblEq=>"prim_dblEq",DblLt=>"prim_dblLt",DblLe=>"prim_dblLe",
    IntToDbl=>"prim_intToDbl",DblFloor=>"prim_dblFloor",DblRound=>"prim_dblRound",DblTruncate=>"prim_dblTruncate",ChrOrd=>"prim_chrOrd",ChrChr=>"prim_chrChr",ChrEq=>"prim_chrEq",ChrLt=>"prim_chrLt",
    StrAppend=>"prim_strAppend",StrLength=>"prim_strLength",StrEq=>"prim_strEq",StrCompare=>"prim_strCompare",StrUnpack=>"prim_strUnpack",StrPack=>"prim_strPack",
    ShowInt=>"prim_showInt",ShowDouble=>"prim_showDouble",ShowChar=>"prim_showChar",ShowString=>"prim_showString",ReadInt=>"prim_readInt",Error=>"prim_error",PutStr=>"prim_putStr",GetLine=>"prim_getLine",Exit=>"prim_exit",Trace=>"prim_trace"
}
/// Formatting starts from Rust's shortest round-trip digits, then normalizes notation.
pub fn show_double(n: f64) -> String {
    if n.is_nan() {
        return "NaN".into();
    }
    if n.is_infinite() {
        return if n.is_sign_negative() {
            "-Infinity"
        } else {
            "Infinity"
        }
        .into();
    }
    if n == 0.0 {
        return if n.is_sign_negative() { "-0.0" } else { "0.0" }.into();
    }
    let raw = format!("{:?}", n.abs());
    let (mantissa, exponent) = raw
        .split_once('e')
        .map_or((raw.as_str(), 0), |(a, b)| (a, b.parse::<i32>().unwrap()));
    let point =
        mantissa.find('.').unwrap_or(mantissa.len()) as i32 + exponent;
    let digits = mantissa.replace('.', "");
    let leading = digits.len() - digits.trim_start_matches('0').len();
    let digits = digits.trim_start_matches('0').trim_end_matches('0');
    let point = point - leading as i32;
    let body = if (1e-4..1e16).contains(&n.abs()) {
        if point <= 0 {
            format!("0.{}{digits}", "0".repeat((-point) as usize))
        } else if point as usize >= digits.len() {
            format!(
                "{digits}{}.0",
                "0".repeat(point as usize - digits.len())
            )
        } else {
            format!(
                "{}.{}",
                &digits[..point as usize],
                &digits[point as usize..]
            )
        }
    } else {
        format!(
            "{}.{}e{:+}",
            &digits[..1],
            if digits.len() == 1 { "0" } else { &digits[1..] },
            point - 1
        )
    };
    if n.is_sign_negative() {
        format!("-{body}")
    } else {
        body
    }
}
/// Quote Unicode text using Fun escapes.
pub fn quote(text: &str, delimiter: char) -> String {
    let mut result = String::from(delimiter);
    for c in text.chars() {
        match c {
            '\n' => result.push_str("\\n"),
            '\t' => result.push_str("\\t"),
            '\0' => result.push_str("\\0"),
            '\\' => result.push_str("\\\\"),
            c if c == delimiter => {
                result.push('\\');
                result.push(c);
            }
            c if c.is_control() => {
                result.push_str(&format!("\\u{{{:x}}}", c as u32))
            }
            c => result.push(c),
        }
    }
    result.push(delimiter);
    result
}
/// Execute a checked primitive, returning errors without terminating the host.
pub fn evaluate(p: Prim, args: &[Value]) -> Result<Value, Failure> {
    use Prim::*;
    let int = |i| match args[i] {
        Value::Int(n) => n,
        _ => unreachable!("typed primitive"),
    };
    let dbl = |i| match args[i] {
        Value::Double(n) => n,
        _ => unreachable!("typed primitive"),
    };
    let chr = |i| match args[i] {
        Value::Char(c) => c,
        _ => unreachable!("typed primitive"),
    };
    let string = |i| match &args[i] {
        Value::String(s) => s.as_str(),
        _ => unreachable!("typed primitive"),
    };
    let integer = |n| Value::Int(normalize(n));
    let double = |n: f64| Value::Double(n);
    let text = |s| Value::String(s);
    let boolean = Value::Bool;
    Ok(match p {
        IntAdd => integer(int(0).wrapping_add(int(1))),
        IntSub => integer(int(0).wrapping_sub(int(1))),
        IntMul => integer(int(0).wrapping_mul(int(1))),
        IntNeg => integer(-int(0)),
        IntQuot | IntRem | IntDiv | IntMod => {
            let (a, b) = (int(0), int(1));
            if b == 0 {
                return Err("divide by zero".into());
            }
            let (mut q, mut r) = (a / b, a % b);
            if matches!(p, IntDiv | IntMod) && r != 0 && (r < 0) != (b < 0)
            {
                q -= 1;
                r += b;
            }
            integer(if matches!(p, IntQuot | IntDiv) { q } else { r })
        }
        IntEq => boolean(int(0) == int(1)),
        IntLt => boolean(int(0) < int(1)),
        IntLe => boolean(int(0) <= int(1)),
        DblAdd => double(dbl(0) + dbl(1)),
        DblSub => double(dbl(0) - dbl(1)),
        DblMul => double(dbl(0) * dbl(1)),
        DblDiv => double(dbl(0) / dbl(1)),
        DblPow => double(dbl(0).powf(dbl(1))),
        DblNeg => double(-dbl(0)),
        DblSqrt => double(dbl(0).sqrt()),
        DblExp => double(dbl(0).exp()),
        DblLog => double(dbl(0).ln()),
        DblSin => double(dbl(0).sin()),
        DblCos => double(dbl(0).cos()),
        DblEq => boolean(dbl(0) == dbl(1)),
        DblLt => boolean(dbl(0) < dbl(1)),
        DblLe => boolean(dbl(0) <= dbl(1)),
        IntToDbl => double(int(0) as f64),
        DblFloor | DblRound | DblTruncate => {
            let n = match p {
                DblFloor => dbl(0).floor(),
                DblRound => dbl(0).round_ties_even(),
                _ => dbl(0).trunc(),
            };
            integer(if n.is_nan() {
                0
            } else {
                (n as i64).clamp(-(1_i64 << 62), (1_i64 << 62) - 1)
            })
        }
        ChrOrd => integer(chr(0) as i64),
        ChrChr => {
            let c = u32::try_from(int(0))
                .ok()
                .and_then(char::from_u32)
                .ok_or("invalid character code point")?;
            Value::Char(c)
        }
        ChrEq => boolean(chr(0) == chr(1)),
        ChrLt => boolean(chr(0) < chr(1)),
        StrAppend => text(format!("{}{}", string(0), string(1))),
        StrLength => integer(string(0).chars().count() as i64),
        StrEq => boolean(string(0) == string(1)),
        StrCompare => integer(match string(0).cmp(string(1)) {
            std::cmp::Ordering::Less => -1,
            std::cmp::Ordering::Equal => 0,
            std::cmp::Ordering::Greater => 1,
        }),
        StrUnpack => Value::Chars(string(0).chars().collect()),
        StrPack => match &args[0] {
            Value::Chars(cs) => text(cs.iter().collect()),
            _ => unreachable!("typed character list"),
        },
        ShowInt => text(int(0).to_string()),
        ShowDouble => text(show_double(dbl(0))),
        ShowChar => text(quote(&chr(0).to_string(), '\'')),
        ShowString => text(quote(string(0), '"')),
        ReadInt => Value::MaybeInt(read_int(string(0))),
        Error => return Err(string(0).into()),
        PutStr => {
            use std::io::Write;
            std::io::stdout()
                .write_all(string(0).as_bytes())
                .map_err(|e| e.to_string())?;
            Value::Unit
        }
        GetLine => {
            use std::io::Write;
            std::io::stdout().flush().map_err(|e| e.to_string())?;
            let mut line = String::new();
            if std::io::stdin()
                .read_line(&mut line)
                .map_err(|e| e.to_string())?
                == 0
            {
                return Err("end of input".into());
            }
            if line.ends_with('\n') {
                line.pop();
                if line.ends_with('\r') {
                    line.pop();
                }
            }
            text(line)
        }
        Trace => {
            trace(string(0))?;
            args[1].clone()
        }
        Exit => return Err(Failure::Exit(int(0) as i32)),
    })
}
/// Match Rust main's broken-pipe policy when entered through a generated C main.
pub(crate) fn init_io() {
    static INIT: std::sync::Once = std::sync::Once::new();
    INIT.call_once(|| {
        // SAFETY: on the supported Unix targets, SIGPIPE is 13 and SIG_IGN is
        // the distinguished handler value 1. This installs no Rust callback.
        unsafe {
            unsafe extern "C" {
                fn signal(number: i32, handler: usize) -> usize;
            }
            signal(13, 1);
        }
    });
}
/// The trace event happens after both strict operands have been evaluated.
pub fn trace(message: &str) -> Result<(), Failure> {
    init_io();
    use std::io::Write;
    std::io::stdout().flush().map_err(|e| e.to_string())?;
    writeln!(std::io::stderr().lock(), "{message}")
        .map_err(|e| e.to_string())?;
    Ok(())
}
}

단순화기는 자신의 재작성이 무엇을 할 수 있는지를 코드 옆에 명시합니다.

#![allow(unused)]
fn main() {
//! A rewrite may delete an unused total RHS, replace an atom by an equal atom,
//! or move a lambda applied exactly once. It may never copy a call, a failing
//! primitive, or an effect, nor move one past another. Allocation failure is
//! outside the source cost model. The oracle checks transitions and trace order.
}

reach::analyze는 안정적인 그래프 순회를 수행합니다. 그 루트에는 관찰 가능한 초기화식이 포함되며, 어떤 전역에 이르는 첫 번째 간선이 그 전역의 증인(witness)이 됩니다:

#![allow(unused)]
fn main() {
use crate::{
    anf::{Anf, Atom, Body, Program, Rhs},
    names::Id,
};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
/// Source-ordered survivors and their first discovery edge. Roots have no predecessor.
pub struct Reach {
    pub kept: Vec<Id>,
    pub witness: BTreeMap<Id, Option<Id>>,
}
/// Visit every reference, including captures hidden in nested lambdas, without recursion.
fn references(body: &Body) -> BTreeSet<Id> {
    let mut found = BTreeSet::new();
    let mut todo = vec![body.as_ref()];
    let mut mark = |a: &Atom| {
        if let Atom::Global(v) = a {
            found.insert(v.clone());
        }
    };
    while let Some(body) = todo.pop() {
        match body {
            Anf::Let(_, rhs, rest) => {
                todo.push(rest);
                match rhs {
                    Rhs::Atom(a) | Rhs::Field(a, _) => mark(a),
                    Rhs::Call(f, args) => {
                        mark(f);
                        for a in args {
                            mark(a);
                        }
                    }
                    Rhs::Con(_, args) | Rhs::Prim(_, args, _) => {
                        for a in args {
                            mark(a);
                        }
                    }
                    Rhs::Lambda(l) => todo.push(&l.body),
                }
            }
            Anf::LetRec(bs, rest) => {
                todo.push(rest);
                for (_, l) in bs {
                    todo.push(&l.body);
                }
            }
            Anf::Join(_, _, body, rest) => {
                todo.push(rest);
                todo.push(body);
            }
            Anf::Case(a, alts) => {
                mark(a);
                for (_, _, body) in alts {
                    todo.push(body);
                }
            }
            Anf::Ret(a) => mark(a),
            Anf::TailCall(f, args) => {
                mark(f);
                for a in args {
                    mark(a);
                }
            }
            Anf::Jump(_, args) => {
                for a in args {
                    mark(a);
                }
            }
            Anf::Fail(_) => {}
        }
    }
    found
}
/// A closure's body does not run during initialization. Calls and failure stay roots.
pub fn discardable(body: &Body) -> bool {
    let mut todo = vec![body.as_ref()];
    while let Some(body) = todo.pop() {
        match body {
            Anf::Let(_, rhs, rest) => {
                if !crate::anf_simplify::discardable(rhs) {
                    return false;
                }
                todo.push(rest);
            }
            Anf::LetRec(_, rest) => todo.push(rest),
            Anf::Ret(_) => {}
            // Control-flow initializers are conservatively retained. A jump may loop.
            _ => return false,
        }
    }
    true
}
/// Filtering preserves the original dependency and initialization order.
pub fn prune(program: &mut Program, reach: &Reach) {
    program
        .globals
        .retain(|(id, _)| reach.witness.contains_key(id));
}
/// Stable diagnostic spelling keeps unique-number churn out of the witness dump.
pub fn display(reach: &Reach, original: usize) -> String {
    use std::fmt::Write;
    let mut text = String::new();
    for id in &reach.kept {
        let parent = reach.witness[id]
            .as_ref()
            .map_or("root", |p| p.text.as_str());
        writeln!(text, "{} <- {parent}", id.text).unwrap();
    }
    writeln!(
        text,
        "kept {}, dropped {}",
        reach.kept.len(),
        original - reach.kept.len()
    )
    .unwrap();
    text
}
/// Follow dependencies from explicit roots and observable initializers in stable order.
pub fn analyze(program: &Program, roots: &[Id]) -> Reach {
    let bodies = program
        .globals
        .iter()
        .map(|(id, body)| (id.clone(), body))
        .collect::<BTreeMap<_, _>>();
    let mut witness = BTreeMap::new();
    let mut todo = VecDeque::new();
    for id in roots.iter().cloned().chain(
        program
            .globals
            .iter()
            .filter(|(_, b)| !discardable(b))
            .map(|(v, _)| v.clone()),
    ) {
        if bodies.contains_key(&id) && !witness.contains_key(&id) {
            witness.insert(id.clone(), None);
            todo.push_back(id);
        }
    }
    while let Some(id) = todo.pop_front() {
        for next in references(bodies[&id]) {
            if bodies.contains_key(&next) && !witness.contains_key(&next) {
                witness.insert(next.clone(), Some(id.clone()));
                todo.push_back(next);
            }
        }
    }
    Reach {
        kept: program
            .globals
            .iter()
            .filter(|(v, _)| witness.contains_key(v))
            .map(|(v, _)| v.clone())
            .collect(),
        witness,
    }
}
}

소거(erasure)는 타입 정보가 아직 남아 있을 때 타입을 분류합니다:

#![allow(unused)]
fn main() {
use crate::{
    ast::Op, core, data::ConId, names::Id, rep::Rep, source::Span,
};
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
/// Expression-result evidence supplies the type of fresh ANF temporaries.
pub struct Expr {
    pub node: Node,
    pub rep: Rep,
}
#[derive(Clone, Debug)]
/// Untyped Core retains strict evaluation order and lexical join points.
pub enum Node {
    Var(Id),
    Global(Id),
    Lit(core::Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(bool, Vec<(Id, Expr)>, Box<Expr>),
    Con(ConId, Vec<Expr>),
    Prim(Op, Vec<Expr>, Span),
    Case(Box<Expr>, Id, Vec<(core::AltCon, Vec<Id>, Expr)>),
    Join(Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(Id, Vec<Expr>),
    Fail(Span),
}
#[derive(Clone, Debug)]
pub struct Program {
    pub globals: Vec<(Id, Expr)>,
    pub reps: BTreeMap<Id, Rep>,
    pub metadata: core::Program,
}
/// Postorder erasure uses a worklist so expression depth does not consume host stack.
fn expression(
    root: &core::Expr,
    program: &core::Program,
    reps: &mut BTreeMap<Id, Rep>,
) -> Expr {
    use core::{Expr as C, Type as T};
    let mut todo = vec![(root, false)];
    let mut results: Vec<(Expr, T)> = vec![];
    while let Some((e, done)) = todo.pop() {
        let children: Vec<&core::Expr> = match e {
            C::Lam(_, e) | C::TyLam(_, e) | C::TyApp(e, _) => vec![e],
            C::App(f, a) => vec![f, a],
            C::Let(g, b) => g
                .binds
                .iter()
                .map(|b| b.rhs.as_ref())
                .chain(std::iter::once(b.as_ref()))
                .collect(),
            C::Con(_, _, es) | C::Prim(_, _, es, _) | C::Jump(_, es, _) => {
                es.iter().collect()
            }
            C::Case(e, _, _, alts) => std::iter::once(e.as_ref())
                .chain(alts.iter().map(|a| &a.rhs))
                .collect(),
            C::Join(_, _, b, r) => vec![b, r],
            C::Match(_) => panic!("match compilation precedes erasure"),
            _ => vec![],
        };
        if !done {
            todo.push((e, true));
            todo.extend(children.into_iter().rev().map(|c| (c, false)));
            continue;
        }
        let start = results.len() - children.len();
        let mut parts = results.split_off(start).into_iter();
        let mut bind = |i: &core::Id| {
            reps.insert(i.name.clone(), representation(&i.ty));
            i.name.clone()
        };
        let (node, ty) = match e {
            C::Var(i) => (Node::Var(i.name.clone()), i.ty.clone()),
            C::Global(i) => (Node::Global(i.name.clone()), i.ty.clone()),
            C::Lit(l) => (
                Node::Lit(l.clone()),
                match l {
                    core::Lit::Int(_) => T::Int,
                    core::Lit::Double(_) => T::Double,
                    core::Lit::Char(_) => T::Char,
                    core::Lit::String(_) => T::String,
                },
            ),
            C::Lam(i, _) => {
                let (b, t) = parts.next().unwrap();
                (Node::Lam(bind(i), Box::new(b)), T::arrow(i.ty.clone(), t))
            }
            C::App(_, _) => {
                let (f, t) = parts.next().unwrap();
                let (a, _) = parts.next().unwrap();
                let T::Arrow(_, t) = t else {
                    panic!("checked Core application")
                };
                (Node::App(Box::new(f), Box::new(a)), *t)
            }
            C::TyLam(v, _) => {
                let (e, t) = parts.next().unwrap();
                (e.node, T::Forall(*v, Box::new(t)))
            }
            C::TyApp(_, a) => {
                let (e, t) = parts.next().unwrap();
                let T::Forall(v, t) = t else {
                    panic!("checked Core instantiation")
                };
                (e.node, t.substitute(v, a))
            }
            C::Let(g, _) => {
                let bs = g
                    .binds
                    .iter()
                    .map(|b| (bind(&b.name), parts.next().unwrap().0))
                    .collect();
                let (b, t) = parts.next().unwrap();
                (Node::Let(g.recursive, bs, Box::new(b)), t)
            }
            C::Con(c, args, _) => {
                let d = &program.constructors[c.index as usize];
                let t = crate::elaborate::instantiate(
                    &d.result,
                    &d.parameters,
                    args,
                );
                (Node::Con(c.clone(), parts.map(|p| p.0).collect()), t)
            }
            C::Prim(op, ts, _, span) => {
                let (mut t, n) = crate::primitives::core_signature(
                    *op,
                    ts,
                    &program.data_types,
                )
                .expect("checked primitive");
                for _ in 0..n {
                    let T::Arrow(_, b) = t else { unreachable!() };
                    t = *b;
                }
                (Node::Prim(*op, parts.map(|p| p.0).collect(), *span), t)
            }
            C::Case(_, i, t, alts) => {
                let e = parts.next().unwrap().0;
                let alts = alts
                    .iter()
                    .map(|a| {
                        (
                            a.con.clone(),
                            a.binders.iter().map(&mut bind).collect(),
                            parts.next().unwrap().0,
                        )
                    })
                    .collect();
                (Node::Case(Box::new(e), bind(i), alts), t.clone())
            }
            C::Join(j, ps, _, _) => {
                let b = parts.next().unwrap().0;
                let (r, t) = parts.next().unwrap();
                (
                    Node::Join(
                        j.clone(),
                        ps.iter().map(&mut bind).collect(),
                        Box::new(b),
                        Box::new(r),
                    ),
                    t,
                )
            }
            C::Jump(j, _, t) => (
                Node::Jump(j.clone(), parts.map(|p| p.0).collect()),
                t.clone(),
            ),
            C::PatternFail(s, t) => (Node::Fail(*s), t.clone()),
            C::Match(_) => unreachable!(),
        };
        results.push((
            Expr {
                node,
                rep: representation(&ty),
            },
            ty,
        ));
    }
    results.pop().unwrap().0
}
/// Evidence is recorded while types exist. Later passes may only copy established facts.
pub fn erase(mut program: core::Program) -> Program {
    let mut reps = BTreeMap::new();
    let globals = program
        .groups
        .iter()
        .flat_map(|g| &g.binds)
        .map(|b| {
            reps.insert(b.name.name.clone(), representation(&b.name.ty));
            (b.name.name.clone(), expression(&b.rhs, &program, &mut reps))
        })
        .collect();
    program.global_types = program
        .groups
        .iter()
        .flat_map(|g| &g.binds)
        .map(|b| {
            (
                b.name.name.clone(),
                crate::core_type::fingerprint(&b.name.ty),
            )
        })
        .collect();
    // Consuming Core must not recursively destroy its nested expressions.
    let mut todo = program
        .groups
        .drain(..)
        .flat_map(|g| g.binds.into_iter().map(|b| *b.rhs))
        .collect::<Vec<_>>();
    while let Some(e) = todo.pop() {
        use core::Expr as C;
        match e {
            C::Lam(_, b) | C::TyLam(_, b) | C::TyApp(b, _) => todo.push(*b),
            C::App(a, b) | C::Join(_, _, a, b) => {
                todo.push(*a);
                todo.push(*b);
            }
            C::Let(g, b) => {
                todo.push(*b);
                todo.extend(g.binds.into_iter().map(|b| *b.rhs));
            }
            C::Con(_, _, es) | C::Prim(_, _, es, _) | C::Jump(_, es, _) => {
                todo.extend(es)
            }
            C::Case(e, _, _, alts) => {
                todo.push(*e);
                todo.extend(alts.into_iter().map(|a| a.rhs));
            }
            C::Match(_) => {
                unreachable!("match compilation precedes erasure")
            }
            _ => {}
        }
    }
    Program {
        globals,
        reps,
        metadata: program,
    }
}
pub(crate) fn literal_rep(l: &core::Lit) -> Rep {
    match l {
        core::Lit::Int(_) => Rep::Int,
        core::Lit::Double(_) => Rep::Double,
        _ => Rep::Word,
    }
}
/// Compact syntax uses an explicit print stack and bounded indentation. Debug's
/// indentation grows with depth and makes even a linear tree quadratic in bytes.
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Expr(&'a Expr),
            Text(String),
        }
        let mut todo = vec![Work::Expr(self)];
        while let Some(work) = todo.pop() {
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Expr(e) => {
                    let mut parts = vec![];
                    match &e.node {
                        Node::Var(v) => write!(f, "{v}")?,
                        Node::Global(v) => write!(f, "@{v}")?,
                        Node::Lit(l) => write!(f, "{l}")?,
                        Node::Lam(v, b) => {
                            write!(f, "(lambda {v}. ")?;
                            parts.push(Work::Expr(b));
                            parts.push(Work::Text(")".into()));
                        }
                        Node::App(a, b) => {
                            write!(f, "(")?;
                            parts.extend([
                                Work::Expr(a),
                                Work::Text(" ".into()),
                                Work::Expr(b),
                                Work::Text(")".into()),
                            ]);
                        }
                        Node::Con(c, es) => {
                            write!(f, "({}", c.name)?;
                            for e in es {
                                parts.push(Work::Text(" ".into()));
                                parts.push(Work::Expr(e));
                            }
                            parts.push(Work::Text(")".into()));
                        }
                        Node::Prim(p, es, _) => {
                            write!(f, "({}", p.text())?;
                            for e in es {
                                parts.push(Work::Text(" ".into()));
                                parts.push(Work::Expr(e));
                            }
                            parts.push(Work::Text(")".into()));
                        }
                        Node::Jump(j, es) => {
                            write!(f, "(jump {j}")?;
                            for e in es {
                                parts.push(Work::Text(" ".into()));
                                parts.push(Work::Expr(e));
                            }
                            parts.push(Work::Text(")".into()));
                        }
                        Node::Let(rec, bs, b) => {
                            write!(
                                f,
                                "(let{} ",
                                if *rec { "rec" } else { "" }
                            )?;
                            for (v, e) in bs {
                                parts.push(Work::Text(format!("{v} = ")));
                                parts.push(Work::Expr(e));
                                parts.push(Work::Text(";\n".into()));
                            }
                            parts.extend([
                                Work::Text("in ".into()),
                                Work::Expr(b),
                                Work::Text(")".into()),
                            ]);
                        }
                        Node::Case(e, v, alts) => {
                            write!(f, "(case ")?;
                            parts.extend([
                                Work::Expr(e),
                                Work::Text(format!(" as {v} {{\n")),
                            ]);
                            for (c, ps, e) in alts {
                                let c = match c {
                                    core::AltCon::Con(c) => c.name.clone(),
                                    core::AltCon::Lit(l) => l.to_string(),
                                    core::AltCon::Default => "_".into(),
                                };
                                parts.push(Work::Text(c));
                                for p in ps {
                                    parts.push(Work::Text(format!(" {p}")));
                                }
                                parts.extend([
                                    Work::Text(" -> ".into()),
                                    Work::Expr(e),
                                    Work::Text(";\n".into()),
                                ]);
                            }
                            parts.push(Work::Text("})".into()));
                        }
                        Node::Join(j, ps, b, r) => {
                            write!(f, "(join {j}")?;
                            for p in ps {
                                write!(f, " {p}")?;
                            }
                            write!(f, " = ")?;
                            parts.extend([
                                Work::Expr(b),
                                Work::Text("\nin ".into()),
                                Work::Expr(r),
                                Work::Text(")".into()),
                            ]);
                        }
                        Node::Fail(s) => write!(f, "fail {s:?}")?,
                    }
                    todo.extend(parts.into_iter().rev());
                }
            }
        }
        Ok(())
    }
}
/// Destroy an owned erased tree without recursively following Box edges.
pub fn discard(root: Expr) {
    let mut todo = vec![root];
    while let Some(e) = todo.pop() {
        match e.node {
            Node::Lam(_, b) => todo.push(*b),
            Node::App(a, b) | Node::Join(_, _, a, b) => {
                todo.push(*a);
                todo.push(*b);
            }
            Node::Let(_, bs, b) => {
                todo.push(*b);
                todo.extend(bs.into_iter().map(|(_, e)| e));
            }
            Node::Con(_, es) | Node::Prim(_, es, _) | Node::Jump(_, es) => {
                todo.extend(es)
            }
            Node::Case(e, _, alts) => {
                todo.push(*e);
                todo.extend(alts.into_iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
/// This is the single classification point. A type variable proves no numeric representation.
pub(crate) fn representation(ty: &core::Type) -> Rep {
    match ty {
        core::Type::Int => Rep::Int,
        core::Type::Double => Rep::Double,
        _ => Rep::Word,
    }
}
}

명시적인 후위 순회 작업 목록이 바인더와 표현식 결과의 증거를 기록합니다. ANF는 과포화 호출에 포함된 각 적용의 결과를 비롯해 이 증거를 새 임시 변수로 복사합니다. 클로저 변환은 이 증거를 보존합니다. 수치 증거가 없거나 서로 모순되면 내부 린트 오류입니다. 인터프리터는 이 테이블을 무시합니다. IR 덤프는 수치 표기를 보여 주므로 독자가 그 사실들을 직접 살펴볼 수 있습니다.

LLVM 방출 과정에서는 별도의 열거형이 현재 저장 방식을 기술합니다:

#![allow(unused)]
fn main() {
use super::*;
use crate::{
    core::Lit,
    flat::{Atom, FuncId},
    runtime_layout as layout,
};
use inkwell::{
    types::BasicMetadataTypeEnum,
    values::{BasicMetadataValueEnum, PointerValue},
};
impl<'ctx> Backend<'ctx, '_> {
    pub(super) fn n(&self, n: u64) -> IntValue<'ctx> {
        self.word.const_int(n, false)
    }
    pub(super) fn external(
        &self,
        name: &str,
        args: &[BasicMetadataTypeEnum<'ctx>],
        result: bool,
    ) -> FunctionValue<'ctx> {
        self.module.get_function(name).unwrap_or_else(|| {
            let ty = if result {
                self.word.fn_type(args, false)
            } else {
                self.context.void_type().fn_type(args, false)
            };
            self.module.add_function(name, ty, None)
        })
    }
    pub(super) fn call(
        &self,
        name: &str,
        args: &[BasicMetadataValueEnum<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        let types = args
            .iter()
            .map(|a| match a {
                BasicMetadataValueEnum::IntValue(v) => v.get_type().into(),
                BasicMetadataValueEnum::PointerValue(v) => {
                    v.get_type().into()
                }
                _ => unreachable!("runtime ABI operands"),
            })
            .collect::<Vec<_>>();
        let call = self.builder.build_call(
            self.external(name, &types, true),
            args,
            "call",
        )?;
        // A tailcc function must regain control to restore its callee-pop frame.
        // LLVM's tail-call pass may otherwise infer a tail transfer into the C ABI.
        call.set_tail_call_kind(
            inkwell::values::LLVMTailCallKind::LLVMTailCallKindNoTail,
        );
        Ok(call.try_as_basic_value().unwrap_basic().into_int_value())
    }
    pub(super) fn void_call(
        &self,
        name: &str,
        args: &[BasicMetadataValueEnum<'ctx>],
    ) -> Result<()> {
        let types = args
            .iter()
            .map(|a| match a {
                BasicMetadataValueEnum::IntValue(v) => v.get_type().into(),
                BasicMetadataValueEnum::PointerValue(v) => {
                    v.get_type().into()
                }
                _ => unreachable!("runtime ABI operands"),
            })
            .collect::<Vec<_>>();
        self.builder.build_call(
            self.external(name, &types, false),
            args,
            "",
        )?;
        Ok(())
    }
    pub(super) fn pointer(
        &self,
        value: IntValue<'ctx>,
    ) -> Result<PointerValue<'ctx>> {
        Ok(self.builder.build_int_to_ptr(
            value,
            self.context.ptr_type(AddressSpace::default()),
            "ptr",
        )?)
    }
    pub(super) fn slot(
        &self,
        pointer: PointerValue<'ctx>,
        index: u64,
    ) -> Result<PointerValue<'ctx>> {
        // SAFETY: GEP indexes i64 words using the checked ABI layout.
        Ok(unsafe {
            self.builder.build_gep(
                self.word,
                pointer,
                &[self.n(index)],
                "slot",
            )?
        })
    }
    pub(super) fn load(
        &self,
        value: IntValue<'ctx>,
        index: u64,
    ) -> Result<IntValue<'ctx>> {
        Ok(self
            .builder
            .build_load(
                self.word,
                self.slot(self.pointer(value)?, index)?,
                "load",
            )?
            .into_int_value())
    }
    pub(super) fn store(
        &self,
        value: IntValue<'ctx>,
        index: u64,
        field: IntValue<'ctx>,
    ) -> Result<()> {
        self.builder
            .build_store(self.slot(self.pointer(value)?, index)?, field)?;
        Ok(())
    }
    pub(super) fn allocation(
        &self,
        kind: u8,
        tag: u32,
        fields: &[IntValue<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        let name = if matches!(kind, layout::DOUBLE | layout::STRING) {
            "rt_alloc_atomic"
        } else {
            "rt_alloc"
        };
        let ptr = self.context.ptr_type(AddressSpace::default());
        let f = self.module.get_function(name).unwrap_or_else(|| {
            self.module.add_function(
                name,
                ptr.fn_type(&[self.word.into()], false),
                None,
            )
        });
        let size = (fields.len() + 1) as u64
            * if name == "rt_alloc" { 1 } else { 8 };
        let p = self
            .builder
            .build_call(f, &[self.n(size).into()], "alloc")?
            .try_as_basic_value()
            .unwrap_basic()
            .into_pointer_value();
        let value = self.builder.build_ptr_to_int(p, self.word, "word")?;
        self.store(
            value,
            0,
            self.n(layout::header(kind, tag, fields.len() as u32)),
        )?;
        for (i, field) in fields.iter().enumerate() {
            self.store(value, i as u64 + 1, *field)?;
        }
        Ok(value)
    }
    pub(super) fn closure(
        &self,
        f: FuncId,
        captures: &[IntValue<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        let code = self.slow[f.0 as usize]
            .as_global_value()
            .as_pointer_value()
            .const_to_int(self.word);
        let mut fields = vec![
            code,
            self.n(self.program.funcs[f.0 as usize].params.len() as u64),
        ];
        fields.extend_from_slice(captures);
        self.allocation(layout::CLOSURE, 0, &fields)
    }
    fn string(&mut self, s: &str) -> IntValue<'ctx> {
        if let Some(v) = self.strings.get(s) {
            return *v;
        }
        let mut bytes = s.as_bytes().to_vec();
        bytes.resize(bytes.len().div_ceil(8) * 8, 0);
        let data = self.context.const_string(&bytes, false);
        let object = self.context.const_struct(
            &[
                self.n(layout::header(
                    layout::STRING,
                    0,
                    1 + (bytes.len() / 8) as u32,
                ))
                .into(),
                self.n(s.len() as u64).into(),
                data.into(),
            ],
            false,
        );
        let g = self.module.add_global(
            object.get_type(),
            None,
            &format!("fun.string{}", self.strings.len()),
        );
        g.set_linkage(Linkage::Private);
        g.set_constant(true);
        g.set_alignment(8);
        g.set_initializer(&object);
        let word = g.as_pointer_value().const_to_int(self.word);
        self.strings.insert(s.into(), word);
        word
    }
    /// Keep static string words in a retained data root table. Strings themselves are read-only.
    pub(super) fn string_roots(&self) {
        if self.strings.is_empty() {
            return;
        }
        let words = self.strings.values().copied().collect::<Vec<_>>();
        let array = self.word.const_array(&words);
        let roots = self.module.add_global(
            array.get_type(),
            None,
            "fun.string_roots",
        );
        roots.set_linkage(Linkage::Internal);
        roots.set_initializer(&array);
        roots.set_alignment(8);
        let ptr = self.context.ptr_type(AddressSpace::default());
        let used = ptr.const_array(&[roots.as_pointer_value()]);
        let keep =
            self.module.add_global(used.get_type(), None, "llvm.used");
        keep.set_linkage(Linkage::Appending);
        keep.set_section(Some("llvm.metadata"));
        keep.set_initializer(&used);
    }
    pub(super) fn literal(&mut self, l: &Lit) -> Result<IntValue<'ctx>> {
        Ok(match l {
            Lit::Int(n) => self.n(layout::integer(*n)),
            Lit::Char(c) => self.n(layout::integer(*c as i64)),
            Lit::String(s) => self.string(s),
            Lit::Double(bits) => {
                self.allocation(layout::DOUBLE, 0, &[self.n(*bits)])?
            }
        })
    }
    pub(super) fn atom(
        &mut self,
        a: &Atom,
        env: &Env<'ctx>,
    ) -> Result<Loc<'ctx>> {
        Ok(match a {
            Atom::Lit(Lit::Int(n)) => Loc::Int(self.n(*n as u64)),
            Atom::Lit(Lit::Double(bits)) => Loc::Double(
                self.context.f64_type().const_float(f64::from_bits(*bits)),
            ),
            Atom::Lit(l) => Loc::Word(self.literal(l)?),
            Atom::Var(v) => env[v],
            Atom::Global(v) => Loc::Word(
                self.builder
                    .build_load(
                        self.word,
                        self.globals[v].as_pointer_value(),
                        "global",
                    )?
                    .into_int_value(),
            ),
        })
    }
    pub(super) fn loc_word(
        &self,
        value: Loc<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        match value {
            Loc::Word(v) => Ok(v),
            Loc::Int(v) => self.retag(v),
            Loc::Double(v) => self.box_double(v),
        }
    }
    pub(super) fn loc_int(
        &self,
        value: Loc<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        match value {
            Loc::Word(v) => self.untag(v),
            Loc::Int(v) => Ok(v),
            Loc::Double(_) => {
                unreachable!("numeric evidence checked by Flat lint")
            }
        }
    }
    pub(super) fn loc_double(
        &self,
        value: Loc<'ctx>,
    ) -> Result<inkwell::values::FloatValue<'ctx>> {
        match value {
            Loc::Word(v) => self.unbox_double(v),
            Loc::Double(v) => Ok(v),
            Loc::Int(_) => {
                unreachable!("numeric evidence checked by Flat lint")
            }
        }
    }
    pub(super) fn atom_word(
        &mut self,
        a: &Atom,
        env: &Env<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        let loc = self.atom(a, env)?;
        self.loc_word(loc)
    }
    pub(super) fn atoms(
        &mut self,
        args: &[Atom],
        env: &Env<'ctx>,
    ) -> Result<Vec<IntValue<'ctx>>> {
        args.iter().map(|a| self.atom_word(a, env)).collect()
    }
    pub(super) fn known(
        &self,
        f: FuncId,
        env: Option<IntValue<'ctx>>,
        args: &[IntValue<'ctx>],
        tail: bool,
    ) -> Result<IntValue<'ctx>> {
        let mut actual: Vec<BasicMetadataValueEnum> =
            vec![env.unwrap_or(self.n(0)).into()];
        actual
            .extend(args.iter().map(|v| BasicMetadataValueEnum::from(*v)));
        let call = self.builder.build_call(
            self.fast[f.0 as usize],
            &actual,
            "known",
        )?;
        call.set_call_convention(TAIL_CC);
        call.set_tail_call(tail);
        if self
            .builder
            .get_insert_block()
            .unwrap()
            .get_parent()
            .unwrap()
            .get_call_conventions()
            != TAIL_CC
        {
            call.set_tail_call_kind(
                inkwell::values::LLVMTailCallKind::LLVMTailCallKindNoTail,
            );
        }
        Ok(call.try_as_basic_value().unwrap_basic().into_int_value())
    }
    pub(super) fn apply(
        &self,
        f: IntValue<'ctx>,
        args: &[IntValue<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        if (1..=4).contains(&args.len()) {
            let mut actual = vec![f.into()];
            actual.extend(
                args.iter().map(|v| BasicMetadataValueEnum::from(*v)),
            );
            return self.call(&format!("rt_apply{}", args.len()), &actual);
        }
        // Allocate in the entry, never in a loop reached by a join back-edge.
        let entry = self
            .builder
            .get_insert_block()
            .unwrap()
            .get_parent()
            .unwrap()
            .get_first_basic_block()
            .unwrap();
        let alloc = self.context.create_builder();
        if let Some(first) = entry.get_first_instruction() {
            alloc.position_before(&first);
        } else {
            alloc.position_at_end(entry);
        }
        let array = alloc.build_alloca(
            self.word.array_type(args.len().max(1) as u32),
            "arguments",
        )?;
        for (i, a) in args.iter().enumerate() {
            self.builder.build_store(self.slot(array, i as u64)?, *a)?;
        }
        self.call(
            "rt_apply",
            &[f.into(), self.n(args.len() as u64).into(), array.into()],
        )
    }
    pub(super) fn box_double(
        &self,
        v: inkwell::values::FloatValue<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        let bits = self
            .builder
            .build_bit_cast(v, self.word, "bits")?
            .into_int_value();
        self.allocation(layout::DOUBLE, 0, &[bits])
    }
    pub(super) fn unbox_double(
        &self,
        v: IntValue<'ctx>,
    ) -> Result<inkwell::values::FloatValue<'ctx>> {
        Ok(self
            .builder
            .build_bit_cast(
                self.load(v, 1)?,
                self.context.f64_type(),
                "double",
            )?
            .into_float_value())
    }
}
/// Storage is local to codegen. All ABI boundaries still carry tagged words.
#[derive(Clone, Copy)]
pub(super) enum Loc<'ctx> {
    Word(IntValue<'ctx>),
    Int(IntValue<'ctx>),
    Double(inkwell::values::FloatValue<'ctx>),
}
}

atom_word는 ABI 용도를 처리합니다. 반면 Integer와 Double 원시 연산의 피연산자는 네이티브 저장 방식을 요청합니다. 태그가 붙은 입력은 해당 용도를 위해 한 번만 디코딩되며, 이미 네이티브 레지스터에 있는 결과는 다음 원시 연산으로 곧바로 흘러갈 수 있습니다. 정수 산술은 네이티브 결과를 저장하기 전에 정규화 헬퍼를 호출합니다:

#![allow(unused)]
fn main() {
fn normalize(&self, value: IntValue<'ctx>) -> Result<IntValue<'ctx>> {
    let bits =
        self.builder
            .build_left_shift(value, self.n(1), "wrap.bits")?;
    Ok(self.builder.build_right_shift(
        bits,
        self.n(1),
        true,
        "wrap.int",
    )?)
}
}

이 코드 생성 변경만 떼어 내어 보기 위해, 원시 연산을 직접 사용하는 Flat 루프를 구성하고 최적화된 LLVM에서 런타임 호출 횟수를 셉니다. 정수 루프에는 런타임 호출이 하나도 필요하지 않습니다. Double 루프에는 호출 경계를 넘는 값을 위한 원자적 할당이 한 번 필요합니다. 오버로드된 소스 프로그램은 별도로 인터프리터와 비교합니다. 그렇게 하면 우리가 실제로 어떤 개선을 구현했는지 정직하게 따질 수 있습니다.

실행해 보기

저장소 루트에서:

$ cd crates/ch23_optimize
$ cargo run -q -- run tests/programs/optimize/trace-sharing.fun --engine=anf --print-steps
once
42
steps: 92
$ cargo run -q -- run tests/programs/optimize/trace-sharing.fun --engine=anf --print-steps --no-simplify
once
42
steps: 171
$ cargo run -q -- dump --pass=reach tests/programs/optimize/trace-sharing.fun

마지막 명령은 유지된 정의와 그 증인을 나열하고, 마지막에 유지된 개수와 제거된 개수를 출력합니다. 수치 바인더 표기를 살펴보려면 dump --pass=anf나 dump --pass=flat을 사용하십시오. 실제로 최적화된 소스 루프를 살펴보려면:

$ cargo run -q -- dump --pass=llvm-opt tests/programs/optimize/double-loop.fun

지역 부동소수점 명령어뿐 아니라 사전 호출도 따라가 보십시오. 소스 함수의 명령어 개수만 비교하면 피호출자 내부의 작업이 가려질 수 있습니다. cargo test --test optimization은 작업 오라클과 트레이스 코퍼스를 실행합니다. cargo test --test native에는 할당 횟수 계약이 포함되어 있습니다.

연습 문제

  1. 준비 운동. trace의 두 번째 인자가 또 다른 트레이스된 식일 때 트레이스 순서를 예측해 보십시오. 모든 엔진에서 이를 확인하십시오.
  2. 중급. 도달 가능성 덤프를 확장하여 선행자 하나가 아니라 루트로부터의 전체 경로를 보여 주도록 하십시오.
  3. 중급. Bool 증거를 추가하고, 비교 결과가 워드 경계를 넘을 때까지 태그 없는 상태로 유지하십시오.
  4. 고급. 네이티브 정수를 조인 매개변수를 통해 전달하십시오. 코드 생성을 변경하기 전에 새로운 저장 불변식을 명시하고 린트를 확장하십시오.
  5. 고급. 작업 오라클에 죽은 저장(dead store)을 세는 카운터를 추가하십시오. 전이 횟수는 보존하지만 불필요한 저장을 수행하는 재작성을 구성해 보십시오.

더 읽을거리

  • 컴파일러에서의 변환과 비용 추론 (Appel 1992).
  • 엄격한 함수형 언어 구현에서의 값 표현 (Leroy 1990).

24장: 정밀 가비지 컬렉터

새 파일

  • rt/src/copying.rs
  • src/liveness.rs
  • src/llvm/roots.rs

변경된 파일

  • src/llvm/control.rs
  • src/llvm/values.rs
  • rt/src/heap.rs
  • rt/src/apply.rs
  • src/llvm/mod.rs
  • src/llvm/primitives.rs
  • src/native.rs
  • src/main.rs
  • rt/src/lib.rs
  • src/lib.rs

동기

지금까지 Boehm 덕분에 우리는 살아 있는 포인터가 모두 어디에 있는지 컬렉터에 알려 주지 않고도 객체를 할당할 수 있었습니다. 언어를 만들어 가는 동안에는 그 점이 유용했습니다. 이제 우리 컴파일러는 어떤 값이 다시 사용될 수 있는지 알고, 우리 런타임은 어떤 필드가 그 값을 담고 있는지 압니다. 우리만의 정밀 컬렉터를 만들 만큼 충분한 정보가 갖춰진 것입니다.

우리는 복사 컬렉터를 사용하겠습니다. 살아 있는 객체는 새 공간으로 옮겨지고, 이전 공간은 다시 사용할 수 있게 됩니다. 이 아이디어는 놀라울 만큼 간결합니다. 문제는 옮겨진 객체를 가리키는 모든 참조가 그 객체를 따라가야 한다는 점입니다.

클로저가 리스트를 캡처한다고 가정해 봅시다. 리스트를 옮긴 뒤에 클로저의 필드를 갱신하더라도, 여전히 레지스터에 이전 주소를 들고 있는 호출자에게는 도움이 되지 않습니다. 그런 참조를 최신 상태로 유지하려면 생성된 코드와 런타임 사이의 협력이 필요합니다(Henderson 2002). Boehm을 계속 쓰면서 이 장을 건너뛰어도 되지만, 컬렉터를 직접 만들어 보는 것은 메모리 이야기 전체가 하나로 맞물리는 모습을 볼 수 있는 훌륭한 방법입니다.

현재의 언어

새로운 Fun 구문은 없습니다. 실행 파일을 빌드할 때 컬렉터를 선택하십시오:

$ cd crates/ch24_gc
$ cargo run -q -- build tests/programs/gc/partial.fun \
    -o ../../build/partial --gc=copying --lint
$ FUN_GC_STRESS=1 ../../build/partial
100
αβγ
add x y z = x <> y <> z

main = do
  let f = add "α"
  let g = f "β"
  print (length (map show [1..100]))
  putStrLn (g "γ")

일반적인 --gc=boehm 모드가 여전히 기본값입니다. 인터프리터는 Rust 표현을 그대로 유지하고, JIT는 Boehm 루트를 그대로 유지합니다. 복사 수집은 네이티브 실행 파일에서 선택할 수 있는 옵션입니다. 이 힙은 하나의 스레드에 속하며, 이는 Fun에 동시성이 없다는 점과 맞아떨어집니다. 수집기 선택은 전역 초기화보다 먼저 이루어집니다.

이론

도달 가능한 그래프 복사하기

힙을 두 영역으로 나눕니다. 할당은 from-space라고 부르는 활성 영역 안에서 포인터를 전진시킵니다. 수집 시에는 루트에서 도달 가능한 객체를 to-space로 복사합니다. 대피시킨 각 객체의 기존 헤더는 포워딩 표식으로, 첫 번째 페이로드 워드는 새 주소로 교체합니다. 그 객체를 가리키는 두 번째 참조는 포워딩 주소를 따라가므로 공유와 순환이 보존됩니다.

여기가 Cheney 알고리즘의 아름다운 부분입니다. 복사된 객체들 자체가 작업 큐를 이루므로, 그래프를 순회하기 위한 별도의 스택이 필요하지 않습니다 (Cheney 1970). 진행 상황을 추적하는 데는 포인터 두 개면 충분합니다.

from-space                         to-space
+------------+                     +----------+-------------+--------+
| FORWARD ---+-------------------->| scanned  | copied,     | free   |
| old object |                     | objects  | not scanned | space  |
+------------+                     +----------+-------------+--------+
                                   ^          ^             ^
                                   start      scan          free

scan 앞에 있는 것은 모두 검사를 마친 것입니다. scan과 free 사이에는 복사는 했지만 아직 검사하지 않은 객체들이 있습니다. 그중 하나를 스캔하면 그 필드를 통해 더 많은 객체를 발견할 수 있으며, 이 객체들은 free 위치에 덧붙입니다. 결국에는 scan이 따라잡습니다. 이로써 살아 있는 그래프 전체를 찾아냈습니다!

이제 두 공간을 맞바꾸고 free 이후부터 할당을 재개합니다. 공유와 순환은 포워딩 주소로 처리되며, 스캔 자체는 결코 Rust의 호출 스택을 통해 재귀하지 않습니다.

객체 종류는 수집기에게 무엇을 검사해야 하는지 알려 줍니다. 생성자 페이로드에는 값이 들어 있습니다. 클로저는 추적되지 않는 코드 주소와 항수로 시작하고, 그 뒤에 캡처가 이어집니다. PAP에는 클로저 하나, 원시 인자 개수, 그리고 인자들이 들어 있습니다. double과 문자열에는 포인터가 들어 있지 않습니다. 인자 없는 생성자와 정수는 최하위 비트가 설정되어 있으므로 추적기가 이를 건너뜁니다. 정적 문자열 주소는 from-space 밖에 있으며 이동하지 않습니다.

루트를 쓰기 가능하게 만들기

섀도 스택(shadow stack)은 머신 스택과 나란히 유지되는 루트 범위의 명시적인 스택입니다. 생성된 각 함수는 태그된 워드 슬롯의 배열을 소유합니다. 네이티브 정수와 부동소수점 중간값에는 루트가 필요하지 않습니다. 포인터일 수 있는 태그된 결과는 슬롯에 저장되며, 이후에 사용할 때마다 다시 읽어 들입니다. 수집이 슬롯을 변경하므로, 다시 읽어 들이면 새 주소를 보게 됩니다.

인자에도 같은 주의가 필요합니다. 뒤쪽의 Double 인자를 박싱하다가 앞쪽 인자가 옮겨질 수 있습니다. 코드 생성은 먼저 변환된 인자를 스크래치 루트에 저장한 다음, 호출을 위해 그 인자들을 모두 다시 불러옵니다. 생성자 할당은 할당기 호출 동안 필드를 보호한 뒤, 재배치된 주소를 새 객체에 저장합니다. Rust가 소유한 인자 버퍼도 같은 등록 계약을 따릅니다. 그 API는 복사된 워드나 원시 ABI 포인터를 반환하며, 수집이 갱신할 수 있는 셀을 가리키는 공유 Rust 슬라이스는 결코 반환하지 않습니다.

반환하는 함수는 자신의 프레임을 제거합니다. 알려진 꼬리 호출은 피호출자에 진입하기 전에 프레임을 제거합니다. 마지막 인자를 불러오는 시점과 피호출자가 자신의 매개변수를 등록하는 시점 사이에는 할당이 없습니다. 따라서 꼬리 재귀는 섀도 프레임을 쌓지 않습니다. 알려지지 않은 호출에는 여전히 21장에서 다룬 eval/apply 제약이 있습니다.

살아 있는 값만 유지하기

반환할 때까지 모든 값을 유지하면 주소 측면에서는 안전하겠지만, 루프 안에서 커다란 죽은 구조를 붙잡아 둘 수 있습니다. 역방향 활성 분석(backward liveness)은 어떤 바인딩이 다시 읽힐 수 있는지 결정합니다. 각 Flat 노드는 자신의 피연산자를 기여합니다. 그 후속 노드들은 자신의 살아 있는 변수를 기여하되, 해당 간선에서 정의되는 바인딩은 제외합니다. 점프의 후속 노드는 그 조인 본문이며, 조인 매개변수는 제거됩니다.

이 방정식들은 단조적입니다. 빈 집합에서 시작하고, 후속자가 바뀌면 선행자를 다시 방문하십시오. 최소 고정점은 일반적인 분기뿐 아니라 조인 역방향 간선도 처리합니다. 코드 생성기는 계산을 내보내기 전에 죽은 바인딩의 슬롯을 비우고 스크래치 범위를 비웁니다. 활성 분석은 제어 흐름에 대해 보수적이지만, 추적되는 0이 아닌 슬롯에는 언제나 태그된 값이 들어 있으며, 박싱되지 않은 수치 중간값은 결코 들어 있지 않습니다.

구현

liveness.rs는 명시적인 작업 목록으로 제어 흐름 그래프를 구성합니다. 솔버도 또 하나의 작업 목록이므로, 소스의 중첩이 Rust 재귀로 바뀌지 않습니다:

#![allow(unused)]
fn main() {
let mut queue = nodes.keys().copied().collect::<VecDeque<_>>();
let mut queued = nodes.keys().copied().collect::<BTreeSet<_>>();
while let Some(id) = queue.pop_front() {
    queued.remove(&id);
    let n = &nodes[&id];
    let mut live = n.uses.clone();
    for (next, defined) in &n.edges {
        live.extend(result.before[next].difference(defined).cloned());
    }
    if result.before[&id] != live {
        result.before.insert(id, live);
        for p in predecessors.get(&id).into_iter().flatten() {
            if queued.insert(*p) {
                queue.push_back(*p);
            }
        }
    }
}
}

llvm/roots.rs는 루트 슬롯을 할당하고, 포인터를 담는 위치를 SSA 워드에서 다시 읽어 들일 수 있는 슬롯으로 바꿉니다. 이 주소들은 활성화 동안 안정적으로 유지됩니다:

#![allow(unused)]
fn main() {
pub(super) fn root_binding(
    &self,
    v: &Id,
    loc: Loc<'ctx>,
) -> Result<Loc<'ctx>> {
    let Some(frame) = &self.frame else {
        return Ok(loc);
    };
    if !matches!(loc, Loc::Word(_) | Loc::Root(_)) {
        return Ok(loc);
    }
    let p = self.slot(frame.array, frame.slots[v])?;
    let w = self.loc_word(loc)?;
    self.builder.build_store(p, w)?;
    Ok(Loc::Root(p))
}
}

런타임의 대피(evacuation) 단계는 고정된 워드를 그대로 두거나, 포워딩 주소를 따라가거나, 객체 하나를 복사합니다. 헤더의 크기에는 페이로드 워드만 포함되므로, 할당할 때는 헤더 자리도 함께 확보합니다:

#![allow(unused)]
fn main() {
fn evacuate(
    from: &mut [Word],
    used: usize,
    to: &mut [Word],
    free: &mut usize,
    w: Word,
) -> Word {
    let base = from.as_ptr() as usize;
    let address = w as usize;
    // Immediates, null placeholders, and static string/code addresses never move.
    if layout::is_immediate(w)
        || address < base
        || address >= base + used * 8
    {
        return w;
    }
    assert_eq!((address - base) % 8, 0);
    let start = (address - base) / 8;
    let header = from[start];
    if layout::kind(header) == layout::FORWARD {
        return from[start + 1];
    }
    let words = 1 + layout::size(header);
    assert!(words >= 2 && start + words <= used);
    let dest = *free;
    to[dest..dest + words].copy_from_slice(&from[start..start + words]);
    *free += words;
    let pointer = to.as_mut_ptr().wrapping_add(dest) as Word;
    from[start] = layout::header(layout::FORWARD, 0, 1);
    from[start + 1] = pointer;
    pointer
}
}

수집기는 루트를 스캔한 다음 to-space의 큐를 소비합니다. 힙은 필요할 때 커지며, 객체를 옮기기 전에 목적지를 먼저 할당합니다. 활성 from-space의 크기를 조정해도 기존 주소가 무효화되는 일은 결코 없습니다.

#![allow(unused)]
fn main() {
fn collect(&mut self, reserve: usize) {
    let capacity = if self.used + reserve > self.from.len() {
        self.from
            .len()
            .saturating_mul(2)
            .max(self.used.checked_add(reserve).expect("heap overflow"))
    } else {
        self.from.len()
    };
    self.spare.resize(capacity, 0);
    let mut free = 0;
    for (&address, &len) in &self.roots {
        for i in 0..len {
            let slot = (address as *mut Word).wrapping_add(i);
            // SAFETY: registered ranges remain writable throughout collection.
            unsafe {
                slot.write(evacuate(
                    &mut self.from,
                    self.used,
                    &mut self.spare,
                    &mut free,
                    slot.read(),
                ));
            }
        }
    }
    let mut scan = 0;
    while scan < free {
        let header = self.spare[scan];
        let size = layout::size(header);
        let first = match layout::kind(header) {
            layout::CON => 0,
            layout::CLOSURE => 2, // code and arity are raw, not values
            layout::PAP => 0,
            layout::DOUBLE | layout::STRING => size,
            _ => panic!("invalid object in to-space"),
        };
        for field in first..size {
            if layout::kind(header) == layout::PAP && field == 1 {
                continue;
            }
            let slot = scan + 1 + field;
            let value = self.spare[slot];
            self.spare[slot] = evacuate(
                &mut self.from,
                self.used,
                &mut self.spare,
                &mut free,
                value,
            );
        }
        scan += size + 1;
    }
    std::mem::swap(&mut self.from, &mut self.spare);
    self.used = free;
    self.collections += 1;
    self.copied_words += free;
}
}

모든 할당 전에 수집을 수행하게 하는 FUN_GC_STRESS=1을 설정하면 낡은 포인터(stale pointer) 버그를 훨씬 쉽게 재현할 수 있습니다. 클로저나 PAP는 그것을 구성하는 단계 사이에 수집이 일어나더라도 살아남아야 합니다. 공유된 참조는 여전히 하나의 객체를 가리켜야 하고, 순환은 순환으로 남아야 하며, 원시 Double 비트를 추적할 필드로 오인해서는 결코 안 됩니다.

실행해 보기

$ cd crates/ch24_gc
$ cargo run -q -- dump --pass=llvm tests/programs/gc/partial.fun --gc=copying
; ... root arrays, rt_roots_push, reloads, rt_roots_pop ...
$ cargo test -p fun-rt-24 --test copying
$ cargo test --test copying

덤프는 LLVM 최적화 이전의 루트 프로토콜을 보여 줍니다. 스트레스 수집은 의도적으로 비용이 큽니다. 점점 커지는 살아 있는 리스트를 반복해서 복사하면 이차(quadratic) 작업이 듭니다. 스트레스 수집은 낡은 주소를 드러내는 데 사용하고, 일반 수집은 따로 측정하십시오. 어느 백엔드도 운영체제 할당 실패로부터의 복구를 약속하지 않습니다.

네이티브 복사 방식 실행 파일은 별도의 copying-only 런타임 아카이브를 링크합니다. 그 빌드에서는 Boehm 임포트와 링크 플래그가 제외됩니다. 일반 런타임 rlib은 여전히 JIT의 Boehm 힙을 지원합니다. just build는 두 아카이브를 모두 준비하고 각각의 네이티브 링크 플래그를 기록합니다. 한 테스트는 수집 스트레스 아래에서 실행 파일의 출력뿐 아니라 동적 의존성도 검사합니다.

힙 확장

추적이 올바른 것만으로는 충분하지 않습니다. 세미스페이스의 거의 전부가 살아 있으면, 각 수집은 큰 그래프를 복사하면서도 겨우 몇 워드만 회수할 수 있습니다. 용량 때문에 촉발된 수집 후에는, 살아 있는 데이터와 대기 중인 할당이 공간의 절반을 넘게 차지할 경우 우리는 공간을 키웁니다. 스트레스 수집은 여전히 모든 할당마다 실행되지만, 그 자체로 확장을 강제하지는 않습니다. 한 회귀 테스트는 공간의 8분의 7을 살아 있는 상태로 유지하고, 할당이 반복되는 동안 복사된 워드의 총량에 상한을 둡니다. 이 상한은 기계마다 다른 타이머에 의존하지 않고 수집기의 작업량을 측정합니다.

연습 문제

  1. 준비 운동. 같은 객체를 가리키는 두 루트를 추적해 보십시오. 왜 복사본이 하나만 만들어지는지, 그리고 각 루트가 결국 어디를 가리키게 되는지 설명하십시오.
  2. 중급. 클로저/PAP 그래프를 그리고 수집기가 건너뛰어야 하는 모든 페이로드 워드를 표시하십시오. 원시 메타데이터와 Double 비트에 대해 하위 비트 검사만으로는 왜 불충분한지 설명하십시오.
  3. 중급. 생존성(liveness) 반복이 두 번 이상 필요한 join 역방향 간선(back-edge)의 예를 드십시오. 집합이 커져 가는 모습을 보이십시오.
  4. 고급. 생존 범위가 겹치지 않는 루트 슬롯을 재사용하십시오. 기존 스트레스 테스트를 유지하고 생성된 프레임 크기를 비교하십시오.
  5. 고급. 세대별 확장을 설계하십시오. 구현하기 전에, 재귀 클로저 패칭에 필요한 쓰기 장벽을 찾아내십시오.

더 읽을거리

Cheney는 비재귀 복사 알고리즘을 제시합니다 (Cheney 1970). Henderson은 다른 면에서는 협조적이지 않은 컴파일 환경에서 명시적 루트를 다루는 방법을 설명합니다 (Henderson 2002). 이 핸드북은 복사, 세대, 동시 수집을 전체적인 맥락 속에서 설명합니다 (Jones et al. 2023). 이 루트 계약을 Boehm과 Weiser의 보수적 접근 방식과 비교해 보십시오 (Boehm and Weiser 1988).

25장: 향후 과제

우리는 계산기에서 출발했습니다. 이제 우리에게는 타입이 추론되고, 대수적 데이터와 레코드, 타입 클래스를 갖춘 언어가 있고, 네이티브 코드를 생성하는 컴파일러도 있습니다. 최적화기와 가비지 컬렉터까지 만들었습니다. 꽤 긴 여정이었습니다!

가장 좋은 점은 이제 이 컴파일러가 마음껏 실험해 볼 수 있는 여러분의 것이 되었다는 것입니다. 여러분은 컴파일러의 결정이 어디에서 내려지는지 알고 있으며, 다른 접근 방식을 시도해 보고 싶을 때 돌아갈 수 있는 더 작은 장별 버전도 가지고 있습니다. 다음으로 Fun을 가지고 탐구해 보면 재미있을 만한 몇 가지 방향을 소개합니다.

다음은 어디로?

명시적 지연과 지연 평가

때로는 정말로 계산을 기술만 해 두고 나중으로 미루고 싶을 때가 있습니다. 저라면 값과 계산을 구분하는 call-by-push-value를 사용해 명시적 지연을 통해 이를 탐구해 보겠습니다 (Levy 2001). 그러면 프로그래머는 계산이 어디에서 중단되고 어디에서 강제되는지 볼 수 있습니다.

결과를 공유하는 것은 또 다른 선택입니다. 필요에 의한 호출에는 서스펜션이 이미 평가되었다는 사실을 기억하는 방법과 재귀적 강제를 처리하는 방법이 필요합니다. 우리의 클로저 표현은 유용한 출발점이지만, 썽크 객체를 추가하는 것만으로는 그 모든 일을 해낼 수 없습니다. STG와 비슷한 중간 언어가 있다면 이러한 연산이 자리 잡을 곳이 생길 것입니다.

모듈과 분할 컴파일

프로그램이 커지면 모든 것을 한 파일에 넣는 일에 아마 지치게 될 것입니다. 모듈이 있으면 정의를 묶고, 인터페이스를 공개하고, 나머지는 숨길 수 있습니다. 분할 컴파일이 있으면 프로그램 전체를 다시 컴파일하지 않고도 한 부분만 다시 빌드할 수 있습니다.

우리는 이미 한 번의 컴파일 안에서는 고유한 이름을 가지고 있습니다. 다음 단계는 어떤 식별자와 타입 정보가 컴파일 사이에 살아남아야 하는지 정하는 것입니다. 클래스가 있으면 이 문제가 특히 흥미로워집니다. 가져온 함수는 인스턴스에 의존할 수 있으므로, 그 함수가 어떤 인스턴스를 보게 되는지에 대한 명확한 규칙이 필요합니다. 명시적 인터페이스를 갖춘 작은 모듈 시스템이 좋은 첫 실험이 될 것입니다.

행, 효과, 클래스

우리의 레코드에는 이름과 고정된 필드가 있습니다. 함수가 x 필드를 가진 레코드라면 무엇이든 받을 수 있다면 어떨까요? 행 다형성(row polymorphism)은 이를 표현하는 한 가지 방법입니다. 효과 행(effect row)은 또 다른 탐구의 길을 열어 주며, 계산이 수행할 수 있는 효과가 무엇인지 타입으로 기술할 수 있게 해 줍니다.

이런 아이디어는 우리가 만든 제약 언어의 범위를 넘어서겠지만, 기본적인 습관은 여전히 통합니다. 표현하고 싶은 작은 프로그램을 찾고, 의도한 타입을 적어 본 다음, 추론에 어떤 정보가 필요한지 알아내십시오.

다중 매개변수 클래스, 함수적 종속성(functional dependency), 타입 패밀리(type family)를 탐구해 볼 수도 있습니다. 우리가 둔 Haskell 98 제약 덕분에 인스턴스 탐색을 감당할 수 있는 수준으로 유지할 수 있습니다 (Simon Marlow 2010). 제약을 하나씩 완화해 보면, 다음 복잡함이 닥치기 전에 그 제약이 우리에게 무엇을 보장해 주고 있었는지 이해할 기회를 얻게 됩니다.

완전한 꼬리 호출

알려진 호출은 이미 LLVM의 꼬리 호출 규약을 사용합니다. 알려지지 않은 호출은 C 런타임을 거치는데, 그곳에서는 호출이 연쇄되면 여전히 스택 프레임이 쌓일 수 있습니다. 이 간극을 메운다면 꽤 뿌듯할 것입니다.

한 가지 방법은 apply 디스패치도 LLVM으로 생성하는 것입니다. 그러면 정확한 호출, PAP 진입, 과포화(oversaturation)가 하나의 꼬리 호출 프로토콜을 공유할 수 있습니다. 흥미로운 경우는 과포화입니다. 첫 번째 호출이 남은 인자를 받을 함수를 반환하는 동안, 그 남은 인자를 어딘가에 보관해 두어야 합니다. eval/apply 모델이 출발점이 되어 줍니다 (Marlow and Peyton Jones 2004).

특수화와 언박싱된 값

산술 연산의 중간값은 네이티브 레지스터에 둘 수 있지만, 호출할 때는 여전히 균일한 워드를 전달합니다. 컴파일러가 호출 지점에서 구체적인 타입을 알고 있다면, 언박싱된 인자를 받는 특수화된 버전을 만들 수 있을 것입니다. 사전을 알고 있으면 메서드 선택을 직접 호출로 바꿀 수도 있습니다.

이 부분에는 실험해 볼 여지가 많습니다. 실행 파일을 엄청나게 키워 버릴 여지도 많습니다. 특수화된 사본은 코드 공간을 차지하므로, 쓸모 있는 실험이라면 절약한 작업량과 늘어난 코드를 모두 측정해야 합니다. 범용 특수화기를 만들기 전에 작은 수치 함수 하나로 시작해서 결과를 살펴보십시오.

최적화와 디버깅

인라이닝은 다음 패스가 단순화할 수 있는 상수, 생성자, 사전 메서드를 드러낼 수 있습니다. 반대로 작업을 중복시킬 수도 있습니다. 23장에서 다룬 트레이스와 머신 단계 수를 이용하면 이 트레이드오프를 조사할 수 있습니다. 최적화는 우리가 실제로 실행할 수 있는 프로그램에 도움이 됨으로써 제자리를 증명해야 합니다.

소스 디버거도 보람 있는 프로젝트가 될 것입니다. 네이티브 실행 파일이 아래에서 돌아가는 동안 Fun 코드를 한 단계씩 따라가는 모습을 상상해 보십시오. DWARF는 기계 명령어를 소스 위치와 연결할 수 있지만, 스코프와 변수 위치도 기술해야 합니다. 최적화 없이 시작하십시오. 최적화기가 변수를 제거한 뒤 그 변수가 어디로 갔는지 설명하는 일은 나중에 즐겨도 되는 문제입니다.

출발점 고르기

이 아이디어 중 하나를 시도해 보려고 컴파일러 전체를 다시 만들 필요는 없습니다. 각각에는 자연스럽게 시작할 수 있는 지점이 있습니다.

방향시작할 곳
지연Core 평가와 클로저 진입
모듈이름이 바뀐 식별자와 컴파일 단위 메타데이터
행 또는 이펙트타입, 제약, 정교화
클래스 확장인스턴스 검사와 제약 풀이
알 수 없는 호출의 TCOFlat 호출, 느린 진입점, 런타임 apply
특수화소거 이전의 타입 증거와 호출 규약
디버깅Flat과 LLVM 생성 단계까지 이어지는 소스 스팬

세부 사항을 찾아봐야 할 때는 IR 레퍼런스와 런타임 ABI를 참고하십시오.

끝까지 오신 것을 축하합니다! 컴파일러를 만드는 일에는 놀라울 만큼 폭넓은 아이디어가 한데 모이며, 여러분은 소스 텍스트에서 실행되는 프로그램에 이르기까지 그 아이디어들을 따라왔습니다. 한때 위협적으로 보였던 장치 중 일부가 이제는 직접 앉아서 만들 수 있는 것처럼 보이기를 바랍니다.

함수형 프로그래밍은 Haskell 2010 이후로 크게 발전했으며, 우리는 겉만 살짝 훑었을 뿐입니다. 함수형 언어와 타입 시스템, 구현에는 아직 발견할 아름다운 아이디어가 남아 있습니다. 호기심이 생기는 것을 하나 골라 시도해 보십시오. 이것은 여러분의 컴파일러 여정의 시작일 뿐입니다.

부록 A: 표면 구문

이 문법은 최종 Fun 언어를 한곳에 모아 둔 것입니다. 이 문법은 레이아웃이 중괄호와 세미콜론을 삽입한 뒤의 소스 구문을 기술합니다. 이름 해석, 카인드, 타입 검사는 아래에 나열한 제약을 부과합니다. 이에 대응하는 판단은 부록 B를 참고하십시오.

문법 읽는 법

따옴표로 묶인 문자열은 종단 기호입니다. |는 대안을 구분하고, [ X ]는 생략 가능함을, { X }는 0회 이상의 반복을 뜻합니다. 괄호는 문법 표현식을 묶습니다. 따옴표로 묶인 대괄호와 중괄호는 리터럴 토큰입니다. varid, conid, 리터럴 토큰은 어휘 절에서 정의합니다. 중괄호가 없는 소스 파일은 바깥쪽 레이아웃 블록을 받으므로, 두 스타일 모두 같은 program 생성 규칙을 가집니다.

선언

program     ::= '{' [ topdecl { ';' topdecl } ] [ ';' ] '}'
topdecl     ::= datadecl | typedecl | classdecl | instdecl
              | fixity | sig | bind

datadecl    ::= 'data' tycon { tyvar } '=' constr
                { '|' constr } [ deriving ]
constr      ::= con { atype }
              | con '{' fielddecl { ',' fielddecl } '}'
fielddecl   ::= varid { ',' varid } '::' type
deriving    ::= 'deriving' conid
              | 'deriving' '(' conid { ',' conid } ')'
typedecl    ::= 'type' conid { tyvar } '=' type

classdecl   ::= 'class' [ context '=>' ] conid tyvar
                [ 'where' '{' [ member { ';' member } ] [ ';' ] '}' ]
member      ::= sig | bind
instdecl    ::= 'instance' [ context '=>' ] conid insthead
                [ 'where' '{' [ bind { ';' bind } ] [ ';' ] '}' ]
insthead    ::= conid | '(' conid { tyvar } ')'
              | '[' tyvar ']' | '(' ')'
              | '(' tyvar ',' tyvar [ ',' tyvar ] ')'
fixity      ::= ('infixl' | 'infixr' | 'infix') digit op { ',' op }
sig         ::= var { ',' var } '::' qualtype

bind        ::= funlhs rhs | pat rhs
funlhs      ::= var { apat } | apat varop apat
rhs         ::= '=' expr [ 'where' decls ]
              | guard '=' expr { guard '=' expr } [ 'where' decls ]
guard       ::= '|' expr
decls       ::= '{' [ localdecl { ';' localdecl } ] [ ';' ] '}'
localdecl   ::= sig | bind

레코드 선언은 정확히 하나의 생성자를 가집니다. 레코드의 레이블은 서로 달라야 하지만, 다른 레코드 타입이 같은 레이블을 다시 쓸 수는 있습니다. 인스턴스 헤드의 변수는 서로 달라야 하며, 컨텍스트는 그 변수들만 언급합니다. 결합성 선언은 최상위 수준에서만 나타나며, 우선순위는 0부터 9까지입니다. deriving할 수 있는 클래스는 Eq, Ord, Show뿐입니다. 패턴 바인딩은 재귀적이지 않습니다. 재귀 바인딩은 디슈거링 후에 함수가 되어야 합니다.

표현식

expr        ::= infixexp [ '::' qualtype ]
infixexp    ::= lexp { op lexp } | '-' infixexp
lexp        ::= '\' apat { apat } '->' expr
              | 'let' decls 'in' expr
              | 'if' expr 'then' expr 'else' expr
              | 'case' expr 'of' '{' alt { ';' alt } [ ';' ] '}'
              | 'do' '{' stmt { ';' stmt } [ ';' ] '}'
              | fexp
fexp        ::= aexp { aexp }
aexp        ::= atom { projection | update }
atom        ::= var | con | literal
              | '(' expr ')'
              | '(' expr ',' expr [ ',' expr ] ')'
              | '[' [ expr { ',' expr } ] ']'
              | '[' expr '..' expr ']'
              | '(' infixexp op ')' | '(' op infixexp ')'
              | '(' PROJ_SECTION varid ')'
              | conid '{' fbind { ',' fbind } '}'
projection  ::= PROJ varid
update      ::= '{' fbind { ',' fbind } '}'
fbind       ::= varid '=' expr
alt         ::= pat ( '->' expr | guard '->' expr
                { guard '->' expr } ) [ 'where' decls ]
stmt        ::= expr | pat '<-' expr | 'let' decls

var         ::= varid | '(' varsym ')'
con         ::= conid | '(' ':' ')' | '[' ']' | '(' ')'
              | '(' ',' ')' | '(' ',' ',' ')'
op          ::= varsym | '`' varid '`' | ':'
varop       ::= varsym | '`' varid '`'

블록의 마지막 do 문장은 표현식이어야 합니다. 맨 생성자 뒤에 레코드 중괄호가 오면 갱신이 아니라 생성입니다. 생성에는 모든 필드가 정확히 한 번씩 필요합니다. 갱신은 레코드의 타입을 보존합니다. 튜플의 항수는 2 또는 3입니다. (- e)는 오른쪽 섹션이 아니라 부정입니다. (&&)와 (||)는 값으로 쓸 수 있지만, 이 연산자들은 재정의할 수 없습니다.

프로젝션과 갱신은 적용보다 더 강하게 결합합니다. 따라서 f r.x는 f (r.x)를 뜻합니다. 적용은 왼쪽으로 결합합니다. 화살표 타입은 오른쪽으로 결합합니다. 연산자 연쇄는 스코프 안의 선언을 사용해 해석하며, 기본값은 infixl 9입니다. 단항 마이너스는 Haskell의 우선순위 6 규칙을 따릅니다. 같은 우선순위에서 결합 방향이 충돌하면 괄호가 필요합니다.

Prelude는 다음과 같은 결합성을 제공합니다.

결합성우선순위연산자
오른쪽9.
왼쪽7*, /, `div`, `mod`, `quot`, `rem`
왼쪽6+, -
오른쪽6<>
오른쪽5:, ++
비결합4==, /=, <, <=, >, >=
왼쪽4<$>, <*>
오른쪽3&&
오른쪽2||
왼쪽1>>=, >>
오른쪽0$

패턴과 타입

pat         ::= lpat [ ':' pat ]
lpat        ::= conid apat { apat } | '-' intlit | apat
apat        ::= varid [ '@' apat ] | '_' | con
              | conid '{' [ fpat { ',' fpat } ] '}'
              | literal | '(' pat ')'
              | '(' pat ',' pat [ ',' pat ] ')'
              | '[' [ pat { ',' pat } ] ']'
fpat        ::= varid '=' pat

qualtype    ::= [ context '=>' ] type
context     ::= pred | '(' pred { ',' pred } ')'
pred        ::= conid tyvar
type        ::= btype [ '->' type ]
btype       ::= atype { atype }
atype       ::= tycon | tyvar | '(' type ')'
              | '(' type ',' type [ ',' type ] ')'
              | '[' type ']'
tycon       ::= conid | '[' ']' | '(' ')'
              | '(' ',' ')' | '(' ',' ',' ')' | '(' '->' ')'
tyvar       ::= varid

하나의 패턴 벡터 안에서 패턴 변수는 서로 달라야 합니다. 생략된 레코드 필드는 와일드카드를 뜻합니다. 패턴 안의 생성자는 포화되어 있어야 합니다. 타입 동의어는 완전히 적용되어야 하며 순환해서는 안 됩니다. 명시적 forall, 카인드 표기, 지연 패턴, 한정된 이름, 사용자 정의 중위 생성자는 없습니다.

어휘 구조와 레이아웃

식별자는 ASCII로 이루어집니다. varid는 [a-z_][A-Za-z0-9_']*에, conid는 [A-Z][A-Za-z0-9_']*에 대응합니다. _ 하나만 있으면 와일드카드입니다. 예약어는 다음과 같습니다.

case class data deriving do else if in infix infixl infixr
instance let of then type where

연산자는 아래의 예약 연산자를 제외하고, 다음 기호 문자들이 최대한 길게 이어진 것입니다. 사용자 정의 연산자는 :로 시작할 수 없습니다.

symbol characters: ! # $ % & * + . / < = > ? @ \ ^ | ~ : -
reserved operators: .. : :: = \ | <- -> @ =>

정수 리터럴은 10진수 또는 0x 16진수이며, 밑줄을 구분자로 쓸 수 있습니다. Double 리터럴은 1.5, 1e10, 2.5e-3처럼 소수점이나 지수를 포함합니다. 부호는 리터럴 토큰 바깥에 있는 구문입니다. 문자 리터럴은 작은따옴표 안에 유니코드 스칼라 하나를 담습니다. 문자열은 큰따옴표 안에 유니코드 텍스트를 담습니다. 둘 다 \n, \t, \\, \', \", \0, \u{…} 이스케이프를 지원합니다. literal은 정수, Double, 문자, 문자열 토큰을 아우릅니다.

줄 주석은 -- 뒤에 기호가 아닌 문자가 오면 시작됩니다. -->는 연산자입니다. 블록 주석은 {-와 -}를 사용하며 중첩될 수 있습니다.

레이아웃 처리 전에, 인접 여부로 프로젝션 점을 구별합니다. 변수, 닫는 구분자, 리터럴 바로 뒤에 있으면서 varid 바로 앞에 있는 점은 PROJ입니다. ( 바로 뒤이면서 varid 앞에 있으면 PROJ_SECTION입니다. 그 밖의 경우에는 일반 연산자입니다. 따라서 f.g는 필드를 프로젝션하고, f . g는 함수를 합성합니다.

레이아웃은 let, where, of, do 뒤와 중괄호가 없는 파일 전체 둘레에 중괄호를 삽입합니다. 들여쓰기가 같으면 선언이나 문장이 서로 구분됩니다. 들여쓰기가 줄어들면 블록이 닫힙니다. 명시적인 중괄호가 우선순위를 가지며, in은 같은 줄에 있더라도 암묵적인 let 블록을 닫습니다. 8장에서 삽입된 토큰을 직접 보여 줍니다. Haskell Report는 그 바탕이 되는 어휘 규칙과 레이아웃 규칙을 제시합니다 (Simon Marlow 2010).

부록 B: 타입 규칙

이 규칙들은 Fun 소스 타입 시스템을 선언적으로 기술합니다. 이 규칙들은 패턴, 바인딩 그룹, 클래스, 레코드, IO를 포함하여 이 책 전반에서 사용한 판단들을 한데 모은 것입니다. 이 규칙들은 어떤 프로그램이 올바르게 타입이 매겨지는지를 기술합니다. 구현은 단일화, 레벨, 제약 풀이를 사용하여 그러한 유도를 찾아냅니다. Core에는 10장에서 소개한, 명시적으로 다형적인 자체 타입 판단이 있습니다.

규칙 읽는 법

분수 꼴의 규칙을 아래에서 위로 읽으면 하나의 질문이 됩니다. 결론을 성립시키려면 선 위의 각 전제를 성립시키십시오. 위에서 아래로 읽으면 하나의 증명이 됩니다. 이 전제들이 그 결론을 정당화합니다. 전제가 없는 규칙은 공리입니다. 괄호 안의 이름은 추가 조건이 아니라 해당 단계를 식별하는 것입니다(Pierce 2002).

우리의 주된 판단은 입니다. 이 판단은 의 변수들이 나열된 스킴을 가지고 의 클래스 술어들이 성립할 때 식 가 타입 를 가진다는 뜻입니다. 세미콜론은 환경들을 구분합니다. 이 세미콜론은 Fun 소스에서는 아무 의미도 없습니다. 예를 들면 다음과 같습니다.

타입 환경은 유한 맵입니다. 타입 환경을 로 확장하면 의 바깥쪽 바인딩이 가려집니다. 윗줄은 시퀀스를 뜻하므로, 는 0개 이상의 타입 변수를 나타냅니다. 아래첨자 가 붙은 규칙은 지시된 시퀀스의 모든 원소에 적용됩니다. 규칙 아래에 적힌 부가 조건은 그 규칙의 일부입니다.

표기법의미
단형 타입: 변수, 생성자, 적용, 또는 화살표
스킴
와 같은 클래스 술어들의 집합
항 변수와 그 스킴
타입 변수와 타입 생성자 및 그 카인드
타입에서의 포획 회피 치환
의 자유 타입 변수
주어진 가정 가 의 모든 술어를 함의함
패턴이 도입하는 단형 변수

타입은 완전히 적용된 동의어를 전개한 뒤에 동일성을 판정합니다. 타입 스킴은 속박 변수의 이름 바꾸기를 무시하고 동일성을 판정합니다. 전역 시그니처는 데이터 생성자, 레코드 필드, 클래스, 인스턴스를 기록합니다. 아래의 모든 규칙은 그 시그니처가 선언 검사를 통과했다고 가정합니다.

카인드와 타입 형성

카인드는 와 입니다. 별은 그 식이 값을 가질 수 있는 타입을 뜻합니다. 카인드 화살표는 항 수준의 함수가 아니라 타입 생성자를 분류합니다.

모든 항의 타입과 생성자 필드는 카인드 를 가집니다. 술어 에서 는 클래스 매개변수의 카인드를 가져야 합니다. 따라서 Functor는 카인드가 인 생성자를 받고, Eq는 카인드가 인 타입을 받습니다. 카인드는 각 타입 선언 컴포넌트 안에서 함께 추론됩니다. 제약 없이 남은 카인드 변수는 가 됩니다. 동의어 순환과 동의어의 부분 적용은, 이후의 단일화가 이를 가려 줄 수 있더라도 오류입니다.

변수, 리터럴, 함수

여기서 는 양화된 변수를 카인드가 일치하는 단형 타입으로 바꿉니다. 생성자도 선언된 타입 스킴을 가지고 같은 규칙을 사용합니다. 리터럴은 클래스 제약 없이 고정된 타입을 가집니다:

람다 매개변수는 본문 전체에서 단형(monomorphic)입니다. id를 두 번 인스턴스화하면 서로 다른 타입을 고를 수 있습니다. 람다로 바인딩된 f를 두 번 사용할 때는 같은 타입을 지켜야 합니다. 패턴 매개변수는 (Abs)를 적용하기 전에 아래의 패턴 규칙을 거칩니다.

Let, 일반화, 시그니처

여기서 입니다. 일반화되는 변수는 또는 에서는 자유 변수이지만 나 에서는 자유 변수가 아닙니다. 가 언급하는 모든 변수는 에도 나타나야 합니다. 솔버는 먼저 를 축약하고 단순화한 뒤, 구체적인 술어를 해소하고 모든 필드 제약을 해결합니다. 이 바인딩의 일반화 스코프 바깥에 있는 변수에 대한 술어는 에 남습니다. 일반화할 수도 없고 인스턴스도 있을 수 없는 잔여 술어는 오류입니다. 기본값 지정(defaulting)도 없고 값 제한(value restriction)도 없습니다.

타입 주석이 없는 재귀 컴포넌트에서는 각 에 새로운 단형 타입 를 할당한 다음, 모든 본문을 바로 그 가정들 아래에서 검사합니다:

풀린 타입들은 (Let)과 같은 제약 조건 및 모호성 조건을 사용하여, 바깥 환경을 기준으로 함께 일반화합니다. 모든 재귀 우변은 디슈거링 후에 구문적으로 함수여야 합니다. 타입 주석이 없는 컴포넌트 안의 재귀는 단형적입니다.

표기는 새로운 경직 변수로 검사되는 약속입니다.

여기서 는 각 양화된 변수를 같은 카인드의 새로운 스콜렘으로 바꿉니다. 이 스콜렘들은 임의의 타입과 단일화될 수 없고 바깥 환경으로 빠져나갈 수도 없습니다. 본문이 요구하는 술어는 와 인스턴스 환경으로부터 도출되어야 합니다. 타입 주석이 달린 바인딩은 명시된 타입 스킴으로 재귀적으로 사용할 수 있으며, 이 덕분에 다형 재귀가 가능합니다. 의존성 분석은 타입 주석이 없는 컴포넌트를 구성하기 전에 타입 주석이 달린 바인딩으로 향하는 간선을 제거합니다. 식 표기는 이 규칙으로 검사된 다음, 식으로 쓰이도록 인스턴스화됩니다. 이 표기는 퍼스트 클래스(first-class) 다형 값을 도입하지 않습니다.

패턴, 가드, 케이스

판단 는 패턴이 타입 의 값과 매치되며 의 변수들을 도입한다는 뜻입니다.

여기서 는 빈 환경입니다. 패턴 환경들의 정의역은 서로 겹치지 않아야 합니다. 리터럴 패턴은 해당 리터럴의 고정된 타입을 가지며 바인딩을 도입하지 않습니다. 레코드 패턴은 선언 순서를 따르는 생성자 패턴이며, 생략된 필드 자리에는 와일드카드가 들어갑니다. 리스트, 튜플, 유닛 패턴은 각자의 일반 생성자를 사용합니다.

가드가 붙은 모든 분기는 case의 결과 타입을 가집니다. 한 행의 where 바인딩은 패턴 환경에서 바인딩 그룹 규칙으로 검사되며, 그 스코프는 해당 행의 가드와 결과 모두에 미칩니다. 함수 등식은 패턴 열마다 인자를 하나씩 도입한 뒤 같은 행 판단을 사용합니다. 모든 등식은 항수(arity)가 같아야 합니다. 망라되지 않은 행과 도달할 수 없는 행은 추가적인 타입 규칙이 아니라 경고를 낳습니다. 매치 실패는 어떤 결과 타입에서든 여전히 발생 가능한 런타임 오류로 남습니다.

비재귀 패턴 바인딩은 먼저 우변과 패턴을 같은 타입으로 검사합니다. 그런 다음 (Let)의 조건에 따라, 도입된 바인더들을 바깥 환경에 대해 일반화합니다. 패턴의 바인더는 그 바인딩의 우변에서는 스코프에 들어 있지 않습니다.

명목적 레코드

로 매개변수화된 레코드 선언에서 필드 타입을 로 쓰고, 가 그 실제 타입 인자들을 치환한다고 합시다. 각 규칙은 레코드 생성자가 이미 식별되어 있을 것을 요구합니다.

선언된 모든 필드는 정확히 한 번씩 나타나야 하며, 알 수 없는 레이블은 없어야 합니다. 필드 슬롯은 선언 순서를 따르지만, 평가 순서는 소스 순서가 결정합니다.

갱신 레이블들은 서로 달라야 하며 에 속해야 합니다. 입력, 필드 타입, 결과에 같은 치환이 나타나므로, 갱신은 타입 매개변수를 바꿀 수 없습니다.

추론은 가 알려지기 전에 를 생성합니다. 을 선언하는 레코드가 하나뿐이라면, 추론은 새로운 매개변수로 그 레코드를 선택합니다. 그렇지 않으면 의 머리 생성자가 정해질 때까지 기다립니다. 솔버는 일반화 전에 이 과정을 반복합니다. 해결되지 않은 필드는 모두 모호한 것으로 간주되며(E0245), 프로그램 전체를 놓고 보면 레코드를 선택할 수 있는 해석이 있더라도 마찬가지입니다. 필드 제약은 절대 스킴에 들어가지 않습니다. (.l)은 \r -> r.l로 검사되므로 같은 제한이 적용됩니다.

클래스 함의와 선언

마지막 규칙은 선언된 인스턴스와, 그 머리를 요구된 술어에 맞추는 카인드가 올바른 치환을 사용합니다. 집합의 함의는 모든 원소의 함의를 요구합니다. 슈퍼클래스 프로젝션과 인스턴스 적용은 정교화 과정에서 쓰이는 사전 증거도 함께 구성합니다.

클래스는 매개변수를 하나 가집니다. 클래스의 메서드 시그니처는 그 매개변수를 언급할 수 있지만, 자체적인 제약을 추가할 수는 없습니다. 메서드의 다른 타입 변수는 암묵적으로 한정됩니다. 기본 메서드는 클래스 술어가 주어진 상태에서 선언된 타입으로 검사됩니다. 슈퍼클래스 관계에는 순환이 없어야 합니다.

인스턴스 헤드는 서로 다른 변수에 적용된 생성자입니다. 인스턴스의 컨텍스트는 그 변수들만 제약합니다. 어떤 두 인스턴스 헤드도 서로 겹쳐서는 안 됩니다. 인스턴스 컨텍스트를 주어진 것으로 삼아, 각 메서드를 그 머리에 맞게 특수화된 클래스 메서드 시그니처와 대조하여 검사하십시오. 필요한 슈퍼클래스 사전도 얻을 수 있어야 합니다. 빠진 메서드에는 기본 구현이 있어야 합니다. 파생(deriving)은 이와 똑같은 검사를 거치는 인스턴스를 생성합니다.

데이터 선언은 생성자 스킴을 도입하는데, 그 매개변수는 전칭 한정되고 필드는 연속된 화살표 인자가 됩니다. 생성자의 결과 타입은 선언된 데이터 타입을 가리킵니다. 이 언어에는 존재 변수, GADT 등식, 생성자별 클래스 컨텍스트가 없습니다.

문법적 설탕, IO, 전체 프로그램

이 형태들에는 별도의 추론 메커니즘이 없습니다. 이들의 타입은 전개 결과가 결정합니다:

표면 형태타입 설명
if b then x else yb : Bool이고, 두 분기는 하나의 타입을 가집니다
a && b, a || b두 피연산자는 모두 Bool이며, 조건부로 평가됩니다
a + b, 그 밖의 연산자연산자 스킴의 적용
-enegate의 적용
(e op), (op e)연산자를 새 매개변수에 적용하는 람다
[a..b]enumFromTo a b이며, 두 경계는 모두 Int입니다
리스트, 튜플, 유닛각각의 데이터 생성자의 적용
where지역 바인딩 그룹

모나드 생성자 에 대해, 바인드 단계에는 타입의 액션과 결과 타입이 인 컨티뉴에이션이 필요합니다.

표현식 문장은 >>를 사용하고, let 문장은 바인딩 규칙을 사용하며, 마지막 표현식이 블록의 결과를 제공합니다. 반박 가능한 바인드 패턴은 error 폴백을 가진 case로 확장됩니다. MonadFail 제약은 없습니다.

IO는 소스 검사기에 대해 추상적이며 카인드를 가집니다. IO는 Monad의 인스턴스이지만, 소스 프로그램은 IO a를 () -> a와 같다고 볼 수 없습니다. 그 표현은 정교화(elaboration) 중에만 드러납니다. 부수 효과가 있는 프리미티브는 Prelude를 컴파일하는 동안에만 보입니다.

완전한 프로그램은 선언의 카인드를 검사하고, 클래스 환경을 검증하고, 바인딩 컴포넌트를 의존성 순서대로 검사하며, 남은 모든 제약을 풉니다. 프로그램의 진입점은 IO () 타입을 가져야 합니다. 해결되지 않은 클래스 제약이나 필드 제약은 하나도 남아서는 안 됩니다. 최상위 초기화식은 그 액션보다 먼저 의존 순서에 따라 실행됩니다. 이러한 평가 요구 사항은 타입 규칙을 보완합니다: 타입이 올바르다고 해서 종료가 보장되거나 명시적 런타임 오류가 없다는 뜻은 아닙니다.

부록 C: 중간 표현

최종 컴파일러에는 실행 가능한 중간 언어가 세 가지 있습니다. 각 언어는 다음 패스가 짊어질 특정한 의무 하나를 덜어 줍니다. 이 정의들은 24장에서 가져온 것이므로 참조 문서와 구현이 함께 유지됩니다.

표현명시적으로 드러내는 것여전히 암묵적으로 남겨 두는 것
System F Core타입, 사전, 생성자, 제어 흐름평가 임시값과 클로저 환경
ANF평가 순서, 중간 결과, 조인, 꼬리 호출캡처된 변수와 적용 디스패치
Flat끌어올려진 코드, 클로저 필드, 알려진 호출과 알려지지 않은 호출물리적 저장소와 기계 명령어

타입이 있는 Core

값 식별자는 자신의 타입을 지닙니다. 타입 적용은 타입 추상화를 인스턴스화합니다. 사전 인자는 평범한 값 인자이므로 TyLam과 TyApp을 지워도 클래스 증거는 지워지지 않습니다.

#![allow(unused)]
fn main() {
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Double(u64),
    Int(i64),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    Con(crate::data::ConId, Vec<Type>, Vec<Expr>),
    Match(Match),
    Case(Box<Expr>, Id, Type, Vec<Alt>),
    Join(crate::names::Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(crate::names::Id, Vec<Expr>, Type),
    PatternFail(Span, Type),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
/// Flat alternatives inspect one constructor or literal, never nested patterns.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AltCon {
    Con(crate::data::ConId),
    Lit(Lit),
    Default,
}
/// Constructor slots become typed binders scoped over the alternative body.
#[derive(Clone, Debug)]
pub struct Alt {
    pub con: AltCon,
    pub binders: Vec<Id>,
    pub rhs: Expr,
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    /// Flat type fingerprints survive erasure for cross-module ABI checks.
    pub global_types:
        std::collections::BTreeMap<crate::names::Id, Vec<String>>,
    pub main: Id,
    pub data_types: Vec<(crate::data::TyConId, crate::kinds::Kind)>,
    pub type_kinds: std::collections::BTreeMap<u32, crate::kinds::Kind>,
    pub constructors: Vec<ConDecl>,
    pub locations: Vec<(Span, String)>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Case(scrut, binder, ty, alts) => {
                write!(f, "(case {scrut} as {binder} -> {ty}")?;
                for alt in alts {
                    write!(f, " [ {:?}", alt.con)?;
                    for id in &alt.binders {
                        write!(f, " {id}")?;
                    }
                    write!(f, " -> {} ]", alt.rhs)?;
                }
                write!(f, ")")
            }
            Self::Join(id, params, body, rest) => {
                write!(f, "(join {id} (")?;
                for p in params {
                    write!(f, " {p}")?;
                }
                write!(f, ") = {body} in {rest})")
            }
            Self::Jump(id, args, ty) => {
                write!(f, "(jump {id} : {ty}")?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::Con(id, types, args) => {
                write!(f, "(con {}", id.name)?;
                for t in types {
                    write!(f, " [{t}]")?;
                }
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Match(m) => {
                write!(f, "(match")?;
                for s in &m.scruts {
                    write!(f, " {}", s.name)?;
                }
                for row in &m.rows {
                    write!(f, " {row}")?;
                }
                write!(f, ")")
            }
            Self::PatternFail(_, ty) => write!(f, "(pattern-fail : {ty})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double(bits) => write!(
                f,
                "{}",
                crate::primitives::show_double(f64::from_bits(*bits))
            ),
            Self::Int(n) => write!(f, "{n}"),
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// Constructor typing is immutable and contains no inference metavariables.
#[derive(Clone, Debug)]
pub struct ConDecl {
    pub labels: Option<Vec<String>>,
    pub id: crate::data::ConId,
    pub tag: u32,
    pub parameters: Vec<u32>,
    pub fields: Vec<Type>,
    pub result: Type,
}
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}
/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}
/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}
/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
impl std::fmt::Display for Pat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(id) => write!(f, "{}", id.name),
            Self::Wild => write!(f, "_"),
            Self::As(id, p) => write!(f, "{}@{p}", id.name),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Con(id, ps) => {
                write!(f, "({}", id.name)?;
                for p in ps {
                    write!(f, " {p}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Row {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for p in &self.pats {
            write!(f, "{p} ")?;
        }
        for local in &self.locals {
            write!(f, "{local} ")?;
        }
        match &self.body {
            RowBody::Plain(e) => write!(f, "-> {e}")?,
            RowBody::Guarded(gs) => {
                for (g, e) in gs {
                    write!(f, "| {g} -> {e} ")?;
                }
            }
        }
        write!(f, " ]")
    }
}
/// Type abstractions erase; a constrained value is an ordinary dictionary function.
pub fn is_function(mut e: &Expr) -> bool {
    while let Expr::TyLam(_, body) = e {
        e = body;
    }
    matches!(e, Expr::Lam(..))
}
/// Dictionary calls introduce global dependencies absent from the source graph.
pub fn order(groups: Vec<BindGroup>) -> Vec<BindGroup> {
    fn refs(e: &Expr, out: &mut std::collections::BTreeSet<u32>) {
        match e {
            Expr::Case(s, _, _, alts) => {
                refs(s, out);
                for a in alts {
                    refs(&a.rhs, out)
                }
            }
            Expr::Join(_, _, b, r) => {
                refs(b, out);
                refs(r, out)
            }
            Expr::Jump(_, args, _) => {
                for a in args {
                    refs(a, out)
                }
            }
            Expr::Global(id) => {
                out.insert(id.unique());
            }
            Expr::Lam(_, b) | Expr::TyLam(_, b) | Expr::TyApp(b, _) => {
                refs(b, out)
            }
            Expr::App(a, b) => {
                refs(a, out);
                refs(b, out);
            }
            Expr::Let(g, b) => {
                for bind in &g.binds {
                    refs(&bind.rhs, out);
                }
                refs(b, out);
            }
            Expr::Con(_, _, args) | Expr::Prim(_, _, args, _) => {
                for a in args {
                    refs(a, out);
                }
            }
            Expr::Match(m) => {
                for row in &m.rows {
                    for g in &row.locals {
                        for b in &g.binds {
                            refs(&b.rhs, out);
                        }
                    }
                    match &row.body {
                        RowBody::Plain(e) => refs(e, out),
                        RowBody::Guarded(gs) => {
                            for (g, e) in gs {
                                refs(g, out);
                                refs(e, out);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
    let binds =
        groups.into_iter().flat_map(|g| g.binds).collect::<Vec<_>>();
    let indices = binds
        .iter()
        .enumerate()
        .map(|(i, b)| (b.name.unique(), i))
        .collect::<std::collections::BTreeMap<_, _>>();
    let edges = binds
        .iter()
        .map(|b| {
            let mut names = std::collections::BTreeSet::new();
            refs(&b.rhs, &mut names);
            names
                .into_iter()
                .filter_map(|n| indices.get(&n).copied())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut binds = binds.into_iter().map(Some).collect::<Vec<_>>();
    crate::scc::components(&edges)
        .into_iter()
        .map(|xs| BindGroup {
            recursive: (xs.len() > 1 || edges[xs[0]].contains(&xs[0]))
                && xs
                    .iter()
                    .all(|i| is_function(&binds[*i].as_ref().unwrap().rhs)),
            binds: xs
                .into_iter()
                .map(|i| binds[i].take().unwrap())
                .collect(),
        })
        .collect()
}
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
use std::collections::BTreeSet;
impl Type {
    pub fn apply(f: Self, a: Self) -> Self {
        match f {
            Self::Con(id, mut args) => {
                args.push(a);
                if id.name == "(->)" && args.len() == 2 {
                    Self::arrow(args.remove(0), args.remove(0))
                } else {
                    Self::Con(id, args)
                }
            }
            f => Self::App(Box::new(f), Box::new(a)),
        }
    }
    pub fn arrow(a: Self, b: Self) -> Self {
        Self::Arrow(Box::new(a), Box::new(b))
    }
    pub fn free(&self) -> BTreeSet<u32> {
        match self {
            Self::Var(v) => BTreeSet::from([*v]),
            Self::Con(_, args) => {
                args.iter().flat_map(Self::free).collect()
            }
            Self::Arrow(a, b) | Self::App(a, b) => {
                let mut vars = a.free();
                vars.extend(b.free());
                vars
            }
            Self::Forall(v, b) => {
                let mut vars = b.free();
                vars.remove(v);
                vars
            }
            _ => BTreeSet::new(),
        }
    }
    fn largest(&self) -> u32 {
        match self {
            Self::Var(v) => *v,
            Self::Con(_, args) => {
                args.iter().map(Self::largest).max().unwrap_or(0)
            }
            Self::Arrow(a, b) | Self::App(a, b) => {
                a.largest().max(b.largest())
            }
            Self::Forall(v, b) => (*v).max(b.largest()),
            _ => 0,
        }
    }
    /// Substitute without capturing a free variable of the replacement.
    // ANCHOR: type-substitution
    pub fn substitute(&self, variable: u32, replacement: &Type) -> Type {
        match self {
            Self::Var(v) if *v == variable => replacement.clone(),
            Self::Con(id, args) => Self::Con(
                id.clone(),
                args.iter()
                    .map(|a| a.substitute(variable, replacement))
                    .collect(),
            ),
            Self::App(a, b) => Self::apply(
                a.substitute(variable, replacement),
                b.substitute(variable, replacement),
            ),
            Self::Arrow(a, b) => Self::arrow(
                a.substitute(variable, replacement),
                b.substitute(variable, replacement),
            ),
            Self::Forall(v, _) if *v == variable => self.clone(),
            Self::Forall(v, body) => {
                let (v, body) = if replacement.free().contains(v) {
                    let fresh = self
                        .largest()
                        .max(replacement.largest())
                        .max(variable)
                        .checked_add(1)
                        .expect("type variable supply exhausted");
                    (fresh, body.substitute(*v, &Self::Var(fresh)))
                } else {
                    (*v, *body.clone())
                };
                Self::Forall(
                    v,
                    Box::new(body.substitute(variable, replacement)),
                )
            }
            _ => self.clone(),
        }
    }
    // ANCHOR_END: type-substitution
    /// Alpha equivalence compares bound identities by their scope, not spelling.
    pub fn equivalent(&self, other: &Self) -> bool {
        fn go(a: &Type, b: &Type, scope: &mut Vec<(u32, u32)>) -> bool {
            match (a, b) {
                (Type::Var(a), Type::Var(b)) => {
                    let left = scope.iter().rposition(|(v, _)| v == a);
                    let right = scope.iter().rposition(|(_, v)| v == b);
                    match (left, right) {
                        (Some(a), Some(b)) => a == b,
                        (None, None) => a == b,
                        _ => false,
                    }
                }
                (Type::Con(a, xs), Type::Con(b, ys)) => {
                    a == b
                        && xs.len() == ys.len()
                        && xs.iter().zip(ys).all(|(x, y)| go(x, y, scope))
                }
                (Type::Arrow(a, b), Type::Arrow(c, d))
                | (Type::App(a, b), Type::App(c, d)) => {
                    go(a, c, scope) && go(b, d, scope)
                }
                (Type::Forall(a, b), Type::Forall(c, d)) => {
                    scope.push((*a, *c));
                    let same = go(b, d, scope);
                    scope.pop();
                    same
                }
                _ => a == b,
            }
        }
        go(self, other, &mut Vec::new())
    }
}
impl std::fmt::Display for Type {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double => write!(f, "Double"),
            Self::Int => write!(f, "Int"),
            Self::Bool => write!(f, "Bool"),
            Self::Char => write!(f, "Char"),
            Self::String => write!(f, "String"),
            Self::Var(v) => write!(f, "t{v}"),
            Self::Con(id, args) => {
                if id.name == "[]" && args.len() == 1 {
                    return write!(f, "[{}]", args[0]);
                }
                if matches!(id.name.as_str(), "(,)" | "(,,)")
                    && args.len() == id.name.len() - 1
                {
                    write!(f, "(")?;
                    for (i, t) in args.iter().enumerate() {
                        if i > 0 {
                            write!(f, ",")?;
                        }
                        write!(f, "{t}")?;
                    }
                    return write!(f, ")");
                }
                write!(f, "{}", id.name)?;
                for t in args {
                    if matches!(t, Self::Arrow(..) | Self::Forall(..))
                        || matches!(t,Self::Con(_,v) if !v.is_empty())
                    {
                        write!(f, " ({t})")?;
                    } else {
                        write!(f, " {t}")?;
                    }
                }
                Ok(())
            }
            Self::Forall(v, b) => write!(f, "forall t{v}. {b}"),
            Self::App(a, b) => write!(f, "{a} ({b})"),
            Self::Arrow(a, b) => {
                if matches!(**a, Self::Arrow(..) | Self::Forall(..)) {
                    write!(f, "({a})")?;
                } else {
                    write!(f, "{a}")?;
                }
                write!(f, " -> {b}")
            }
        }
    }
}
/// A flat, alpha-normalized key protects imported ABIs when source is rechecked.
/// Both traversal and ownership use vectors, without recursively cloning types.
pub fn fingerprint(ty: &Type) -> Vec<String> {
    enum Work<'a> {
        Visit(&'a Type),
        Leave,
    }
    let mut work = vec![Work::Visit(ty)];
    let mut scope = vec![];
    let mut key = vec![];
    while let Some(item) = work.pop() {
        let Work::Visit(ty) = item else {
            scope.pop();
            continue;
        };
        match ty {
            Type::Var(n) => {
                key.push(match scope.iter().rev().position(|v| v == n) {
                    Some(depth) => format!("bound:{depth}"),
                    None => format!("free:{n}"),
                })
            }
            Type::Con(id, args) => {
                key.push(format!("con:{}:{}", id.name, args.len()));
                work.extend(args.iter().rev().map(Work::Visit));
            }
            Type::Arrow(a, b) | Type::App(a, b) => {
                key.push(
                    if matches!(ty, Type::Arrow(..)) {
                        "arrow"
                    } else {
                        "app"
                    }
                    .into(),
                );
                work.push(Work::Visit(b));
                work.push(Work::Visit(a));
            }
            Type::Forall(n, body) => {
                key.push("forall".into());
                scope.push(*n);
                work.push(Work::Leave);
                work.push(Work::Visit(body));
            }
            Type::Int => key.push("Int".into()),
            Type::Double => key.push("Double".into()),
            Type::Bool => key.push("Bool".into()),
            Type::Char => key.push("Char".into()),
            Type::String => key.push("String".into()),
        }
    }
    key
}
/// Core type applications retain kinds in the program's binder table.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Type {
    Double,
    Int,
    Bool,
    Char,
    String,
    Var(u32),
    Con(crate::data::TyConId, Vec<Type>),
    App(Box<Type>, Box<Type>),
    Arrow(Box<Type>, Box<Type>),
    Forall(u32, Box<Type>),
}
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Double(u64),
    Int(i64),
    Char(char),
    String(String),
}
/// Flat alternatives inspect one constructor or literal, never nested patterns.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AltCon {
    Con(crate::data::ConId),
    Lit(Lit),
    Default,
}
/// Constructor slots become typed binders scoped over the alternative body.
#[derive(Clone, Debug)]
pub struct Alt {
    pub con: AltCon,
    pub binders: Vec<Id>,
    pub rhs: Expr,
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    /// Flat type fingerprints survive erasure for cross-module ABI checks.
    pub global_types:
        std::collections::BTreeMap<crate::names::Id, Vec<String>>,
    pub main: Id,
    pub data_types: Vec<(crate::data::TyConId, crate::kinds::Kind)>,
    pub type_kinds: std::collections::BTreeMap<u32, crate::kinds::Kind>,
    pub constructors: Vec<ConDecl>,
    pub locations: Vec<(Span, String)>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Case(scrut, binder, ty, alts) => {
                write!(f, "(case {scrut} as {binder} -> {ty}")?;
                for alt in alts {
                    write!(f, " [ {:?}", alt.con)?;
                    for id in &alt.binders {
                        write!(f, " {id}")?;
                    }
                    write!(f, " -> {} ]", alt.rhs)?;
                }
                write!(f, ")")
            }
            Self::Join(id, params, body, rest) => {
                write!(f, "(join {id} (")?;
                for p in params {
                    write!(f, " {p}")?;
                }
                write!(f, ") = {body} in {rest})")
            }
            Self::Jump(id, args, ty) => {
                write!(f, "(jump {id} : {ty}")?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::Con(id, types, args) => {
                write!(f, "(con {}", id.name)?;
                for t in types {
                    write!(f, " [{t}]")?;
                }
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Match(m) => {
                write!(f, "(match")?;
                for s in &m.scruts {
                    write!(f, " {}", s.name)?;
                }
                for row in &m.rows {
                    write!(f, " {row}")?;
                }
                write!(f, ")")
            }
            Self::PatternFail(_, ty) => write!(f, "(pattern-fail : {ty})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double(bits) => write!(
                f,
                "{}",
                crate::primitives::show_double(f64::from_bits(*bits))
            ),
            Self::Int(n) => write!(f, "{n}"),
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// Constructor typing is immutable and contains no inference metavariables.
#[derive(Clone, Debug)]
pub struct ConDecl {
    pub labels: Option<Vec<String>>,
    pub id: crate::data::ConId,
    pub tag: u32,
    pub parameters: Vec<u32>,
    pub fields: Vec<Type>,
    pub result: Type,
}
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}
/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}
/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}
/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
impl std::fmt::Display for Pat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(id) => write!(f, "{}", id.name),
            Self::Wild => write!(f, "_"),
            Self::As(id, p) => write!(f, "{}@{p}", id.name),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Con(id, ps) => {
                write!(f, "({}", id.name)?;
                for p in ps {
                    write!(f, " {p}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Row {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for p in &self.pats {
            write!(f, "{p} ")?;
        }
        for local in &self.locals {
            write!(f, "{local} ")?;
        }
        match &self.body {
            RowBody::Plain(e) => write!(f, "-> {e}")?,
            RowBody::Guarded(gs) => {
                for (g, e) in gs {
                    write!(f, "| {g} -> {e} ")?;
                }
            }
        }
        write!(f, " ]")
    }
}
/// Type abstractions erase; a constrained value is an ordinary dictionary function.
pub fn is_function(mut e: &Expr) -> bool {
    while let Expr::TyLam(_, body) = e {
        e = body;
    }
    matches!(e, Expr::Lam(..))
}
/// Dictionary calls introduce global dependencies absent from the source graph.
pub fn order(groups: Vec<BindGroup>) -> Vec<BindGroup> {
    fn refs(e: &Expr, out: &mut std::collections::BTreeSet<u32>) {
        match e {
            Expr::Case(s, _, _, alts) => {
                refs(s, out);
                for a in alts {
                    refs(&a.rhs, out)
                }
            }
            Expr::Join(_, _, b, r) => {
                refs(b, out);
                refs(r, out)
            }
            Expr::Jump(_, args, _) => {
                for a in args {
                    refs(a, out)
                }
            }
            Expr::Global(id) => {
                out.insert(id.unique());
            }
            Expr::Lam(_, b) | Expr::TyLam(_, b) | Expr::TyApp(b, _) => {
                refs(b, out)
            }
            Expr::App(a, b) => {
                refs(a, out);
                refs(b, out);
            }
            Expr::Let(g, b) => {
                for bind in &g.binds {
                    refs(&bind.rhs, out);
                }
                refs(b, out);
            }
            Expr::Con(_, _, args) | Expr::Prim(_, _, args, _) => {
                for a in args {
                    refs(a, out);
                }
            }
            Expr::Match(m) => {
                for row in &m.rows {
                    for g in &row.locals {
                        for b in &g.binds {
                            refs(&b.rhs, out);
                        }
                    }
                    match &row.body {
                        RowBody::Plain(e) => refs(e, out),
                        RowBody::Guarded(gs) => {
                            for (g, e) in gs {
                                refs(g, out);
                                refs(e, out);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
    let binds =
        groups.into_iter().flat_map(|g| g.binds).collect::<Vec<_>>();
    let indices = binds
        .iter()
        .enumerate()
        .map(|(i, b)| (b.name.unique(), i))
        .collect::<std::collections::BTreeMap<_, _>>();
    let edges = binds
        .iter()
        .map(|b| {
            let mut names = std::collections::BTreeSet::new();
            refs(&b.rhs, &mut names);
            names
                .into_iter()
                .filter_map(|n| indices.get(&n).copied())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut binds = binds.into_iter().map(Some).collect::<Vec<_>>();
    crate::scc::components(&edges)
        .into_iter()
        .map(|xs| BindGroup {
            recursive: (xs.len() > 1 || edges[xs[0]].contains(&xs[0]))
                && xs
                    .iter()
                    .all(|i| is_function(&binds[*i].as_ref().unwrap().rhs)),
            binds: xs
                .into_iter()
                .map(|i| binds[i].take().unwrap())
                .collect(),
        })
        .collect()
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    Con(crate::data::ConId, Vec<Type>, Vec<Expr>),
    Match(Match),
    Case(Box<Expr>, Id, Type, Vec<Alt>),
    Join(crate::names::Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(crate::names::Id, Vec<Expr>, Type),
    PatternFail(Span, Type),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
}

Match는 정교화된 Core에 속합니다. 매치 컴파일은 이를 제거하고 평탄한 Case 대안과 어휘적 Join/Jump를 남깁니다. 생성자와 프리미티브는 포화되어 있습니다. 점프는 완전히 적용되며 자신의 조인 스코프 안에서 꼬리 위치에만 나타납니다. 린트 패스는 소스 타입 검사기와 독립적으로 이러한 속성을 검사합니다.

#![allow(unused)]
fn main() {
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Double(u64),
    Int(i64),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    Con(crate::data::ConId, Vec<Type>, Vec<Expr>),
    Match(Match),
    Case(Box<Expr>, Id, Type, Vec<Alt>),
    Join(crate::names::Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(crate::names::Id, Vec<Expr>, Type),
    PatternFail(Span, Type),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
/// Constructor slots become typed binders scoped over the alternative body.
#[derive(Clone, Debug)]
pub struct Alt {
    pub con: AltCon,
    pub binders: Vec<Id>,
    pub rhs: Expr,
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    /// Flat type fingerprints survive erasure for cross-module ABI checks.
    pub global_types:
        std::collections::BTreeMap<crate::names::Id, Vec<String>>,
    pub main: Id,
    pub data_types: Vec<(crate::data::TyConId, crate::kinds::Kind)>,
    pub type_kinds: std::collections::BTreeMap<u32, crate::kinds::Kind>,
    pub constructors: Vec<ConDecl>,
    pub locations: Vec<(Span, String)>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Case(scrut, binder, ty, alts) => {
                write!(f, "(case {scrut} as {binder} -> {ty}")?;
                for alt in alts {
                    write!(f, " [ {:?}", alt.con)?;
                    for id in &alt.binders {
                        write!(f, " {id}")?;
                    }
                    write!(f, " -> {} ]", alt.rhs)?;
                }
                write!(f, ")")
            }
            Self::Join(id, params, body, rest) => {
                write!(f, "(join {id} (")?;
                for p in params {
                    write!(f, " {p}")?;
                }
                write!(f, ") = {body} in {rest})")
            }
            Self::Jump(id, args, ty) => {
                write!(f, "(jump {id} : {ty}")?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::Con(id, types, args) => {
                write!(f, "(con {}", id.name)?;
                for t in types {
                    write!(f, " [{t}]")?;
                }
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Match(m) => {
                write!(f, "(match")?;
                for s in &m.scruts {
                    write!(f, " {}", s.name)?;
                }
                for row in &m.rows {
                    write!(f, " {row}")?;
                }
                write!(f, ")")
            }
            Self::PatternFail(_, ty) => write!(f, "(pattern-fail : {ty})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double(bits) => write!(
                f,
                "{}",
                crate::primitives::show_double(f64::from_bits(*bits))
            ),
            Self::Int(n) => write!(f, "{n}"),
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// Constructor typing is immutable and contains no inference metavariables.
#[derive(Clone, Debug)]
pub struct ConDecl {
    pub labels: Option<Vec<String>>,
    pub id: crate::data::ConId,
    pub tag: u32,
    pub parameters: Vec<u32>,
    pub fields: Vec<Type>,
    pub result: Type,
}
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}
/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}
/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}
/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
impl std::fmt::Display for Pat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(id) => write!(f, "{}", id.name),
            Self::Wild => write!(f, "_"),
            Self::As(id, p) => write!(f, "{}@{p}", id.name),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Con(id, ps) => {
                write!(f, "({}", id.name)?;
                for p in ps {
                    write!(f, " {p}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Row {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for p in &self.pats {
            write!(f, "{p} ")?;
        }
        for local in &self.locals {
            write!(f, "{local} ")?;
        }
        match &self.body {
            RowBody::Plain(e) => write!(f, "-> {e}")?,
            RowBody::Guarded(gs) => {
                for (g, e) in gs {
                    write!(f, "| {g} -> {e} ")?;
                }
            }
        }
        write!(f, " ]")
    }
}
/// Type abstractions erase; a constrained value is an ordinary dictionary function.
pub fn is_function(mut e: &Expr) -> bool {
    while let Expr::TyLam(_, body) = e {
        e = body;
    }
    matches!(e, Expr::Lam(..))
}
/// Dictionary calls introduce global dependencies absent from the source graph.
pub fn order(groups: Vec<BindGroup>) -> Vec<BindGroup> {
    fn refs(e: &Expr, out: &mut std::collections::BTreeSet<u32>) {
        match e {
            Expr::Case(s, _, _, alts) => {
                refs(s, out);
                for a in alts {
                    refs(&a.rhs, out)
                }
            }
            Expr::Join(_, _, b, r) => {
                refs(b, out);
                refs(r, out)
            }
            Expr::Jump(_, args, _) => {
                for a in args {
                    refs(a, out)
                }
            }
            Expr::Global(id) => {
                out.insert(id.unique());
            }
            Expr::Lam(_, b) | Expr::TyLam(_, b) | Expr::TyApp(b, _) => {
                refs(b, out)
            }
            Expr::App(a, b) => {
                refs(a, out);
                refs(b, out);
            }
            Expr::Let(g, b) => {
                for bind in &g.binds {
                    refs(&bind.rhs, out);
                }
                refs(b, out);
            }
            Expr::Con(_, _, args) | Expr::Prim(_, _, args, _) => {
                for a in args {
                    refs(a, out);
                }
            }
            Expr::Match(m) => {
                for row in &m.rows {
                    for g in &row.locals {
                        for b in &g.binds {
                            refs(&b.rhs, out);
                        }
                    }
                    match &row.body {
                        RowBody::Plain(e) => refs(e, out),
                        RowBody::Guarded(gs) => {
                            for (g, e) in gs {
                                refs(g, out);
                                refs(e, out);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
    let binds =
        groups.into_iter().flat_map(|g| g.binds).collect::<Vec<_>>();
    let indices = binds
        .iter()
        .enumerate()
        .map(|(i, b)| (b.name.unique(), i))
        .collect::<std::collections::BTreeMap<_, _>>();
    let edges = binds
        .iter()
        .map(|b| {
            let mut names = std::collections::BTreeSet::new();
            refs(&b.rhs, &mut names);
            names
                .into_iter()
                .filter_map(|n| indices.get(&n).copied())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut binds = binds.into_iter().map(Some).collect::<Vec<_>>();
    crate::scc::components(&edges)
        .into_iter()
        .map(|xs| BindGroup {
            recursive: (xs.len() > 1 || edges[xs[0]].contains(&xs[0]))
                && xs
                    .iter()
                    .all(|i| is_function(&binds[*i].as_ref().unwrap().rhs)),
            binds: xs
                .into_iter()
                .map(|i| binds[i].take().unwrap())
                .collect(),
        })
        .collect()
}
/// Flat alternatives inspect one constructor or literal, never nested patterns.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AltCon {
    Con(crate::data::ConId),
    Lit(Lit),
    Default,
}
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Double(u64),
    Int(i64),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    Con(crate::data::ConId, Vec<Type>, Vec<Expr>),
    Match(Match),
    Case(Box<Expr>, Id, Type, Vec<Alt>),
    Join(crate::names::Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(crate::names::Id, Vec<Expr>, Type),
    PatternFail(Span, Type),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
/// Flat alternatives inspect one constructor or literal, never nested patterns.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AltCon {
    Con(crate::data::ConId),
    Lit(Lit),
    Default,
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    /// Flat type fingerprints survive erasure for cross-module ABI checks.
    pub global_types:
        std::collections::BTreeMap<crate::names::Id, Vec<String>>,
    pub main: Id,
    pub data_types: Vec<(crate::data::TyConId, crate::kinds::Kind)>,
    pub type_kinds: std::collections::BTreeMap<u32, crate::kinds::Kind>,
    pub constructors: Vec<ConDecl>,
    pub locations: Vec<(Span, String)>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Case(scrut, binder, ty, alts) => {
                write!(f, "(case {scrut} as {binder} -> {ty}")?;
                for alt in alts {
                    write!(f, " [ {:?}", alt.con)?;
                    for id in &alt.binders {
                        write!(f, " {id}")?;
                    }
                    write!(f, " -> {} ]", alt.rhs)?;
                }
                write!(f, ")")
            }
            Self::Join(id, params, body, rest) => {
                write!(f, "(join {id} (")?;
                for p in params {
                    write!(f, " {p}")?;
                }
                write!(f, ") = {body} in {rest})")
            }
            Self::Jump(id, args, ty) => {
                write!(f, "(jump {id} : {ty}")?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::Con(id, types, args) => {
                write!(f, "(con {}", id.name)?;
                for t in types {
                    write!(f, " [{t}]")?;
                }
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Match(m) => {
                write!(f, "(match")?;
                for s in &m.scruts {
                    write!(f, " {}", s.name)?;
                }
                for row in &m.rows {
                    write!(f, " {row}")?;
                }
                write!(f, ")")
            }
            Self::PatternFail(_, ty) => write!(f, "(pattern-fail : {ty})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double(bits) => write!(
                f,
                "{}",
                crate::primitives::show_double(f64::from_bits(*bits))
            ),
            Self::Int(n) => write!(f, "{n}"),
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// Constructor typing is immutable and contains no inference metavariables.
#[derive(Clone, Debug)]
pub struct ConDecl {
    pub labels: Option<Vec<String>>,
    pub id: crate::data::ConId,
    pub tag: u32,
    pub parameters: Vec<u32>,
    pub fields: Vec<Type>,
    pub result: Type,
}
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}
/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}
/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}
/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
impl std::fmt::Display for Pat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(id) => write!(f, "{}", id.name),
            Self::Wild => write!(f, "_"),
            Self::As(id, p) => write!(f, "{}@{p}", id.name),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Con(id, ps) => {
                write!(f, "({}", id.name)?;
                for p in ps {
                    write!(f, " {p}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Row {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for p in &self.pats {
            write!(f, "{p} ")?;
        }
        for local in &self.locals {
            write!(f, "{local} ")?;
        }
        match &self.body {
            RowBody::Plain(e) => write!(f, "-> {e}")?,
            RowBody::Guarded(gs) => {
                for (g, e) in gs {
                    write!(f, "| {g} -> {e} ")?;
                }
            }
        }
        write!(f, " ]")
    }
}
/// Type abstractions erase; a constrained value is an ordinary dictionary function.
pub fn is_function(mut e: &Expr) -> bool {
    while let Expr::TyLam(_, body) = e {
        e = body;
    }
    matches!(e, Expr::Lam(..))
}
/// Dictionary calls introduce global dependencies absent from the source graph.
pub fn order(groups: Vec<BindGroup>) -> Vec<BindGroup> {
    fn refs(e: &Expr, out: &mut std::collections::BTreeSet<u32>) {
        match e {
            Expr::Case(s, _, _, alts) => {
                refs(s, out);
                for a in alts {
                    refs(&a.rhs, out)
                }
            }
            Expr::Join(_, _, b, r) => {
                refs(b, out);
                refs(r, out)
            }
            Expr::Jump(_, args, _) => {
                for a in args {
                    refs(a, out)
                }
            }
            Expr::Global(id) => {
                out.insert(id.unique());
            }
            Expr::Lam(_, b) | Expr::TyLam(_, b) | Expr::TyApp(b, _) => {
                refs(b, out)
            }
            Expr::App(a, b) => {
                refs(a, out);
                refs(b, out);
            }
            Expr::Let(g, b) => {
                for bind in &g.binds {
                    refs(&bind.rhs, out);
                }
                refs(b, out);
            }
            Expr::Con(_, _, args) | Expr::Prim(_, _, args, _) => {
                for a in args {
                    refs(a, out);
                }
            }
            Expr::Match(m) => {
                for row in &m.rows {
                    for g in &row.locals {
                        for b in &g.binds {
                            refs(&b.rhs, out);
                        }
                    }
                    match &row.body {
                        RowBody::Plain(e) => refs(e, out),
                        RowBody::Guarded(gs) => {
                            for (g, e) in gs {
                                refs(g, out);
                                refs(e, out);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
    let binds =
        groups.into_iter().flat_map(|g| g.binds).collect::<Vec<_>>();
    let indices = binds
        .iter()
        .enumerate()
        .map(|(i, b)| (b.name.unique(), i))
        .collect::<std::collections::BTreeMap<_, _>>();
    let edges = binds
        .iter()
        .map(|b| {
            let mut names = std::collections::BTreeSet::new();
            refs(&b.rhs, &mut names);
            names
                .into_iter()
                .filter_map(|n| indices.get(&n).copied())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut binds = binds.into_iter().map(Some).collect::<Vec<_>>();
    crate::scc::components(&edges)
        .into_iter()
        .map(|xs| BindGroup {
            recursive: (xs.len() > 1 || edges[xs[0]].contains(&xs[0]))
                && xs
                    .iter()
                    .all(|i| is_function(&binds[*i].as_ref().unwrap().rhs)),
            binds: xs
                .into_iter()
                .map(|i| binds[i].take().unwrap())
                .collect(),
        })
        .collect()
}
/// Constructor slots become typed binders scoped over the alternative body.
#[derive(Clone, Debug)]
pub struct Alt {
    pub con: AltCon,
    pub binders: Vec<Id>,
    pub rhs: Expr,
}
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Double(u64),
    Int(i64),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    Con(crate::data::ConId, Vec<Type>, Vec<Expr>),
    Match(Match),
    Case(Box<Expr>, Id, Type, Vec<Alt>),
    Join(crate::names::Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(crate::names::Id, Vec<Expr>, Type),
    PatternFail(Span, Type),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
/// Flat alternatives inspect one constructor or literal, never nested patterns.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AltCon {
    Con(crate::data::ConId),
    Lit(Lit),
    Default,
}
/// Constructor slots become typed binders scoped over the alternative body.
#[derive(Clone, Debug)]
pub struct Alt {
    pub con: AltCon,
    pub binders: Vec<Id>,
    pub rhs: Expr,
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    /// Flat type fingerprints survive erasure for cross-module ABI checks.
    pub global_types:
        std::collections::BTreeMap<crate::names::Id, Vec<String>>,
    pub main: Id,
    pub data_types: Vec<(crate::data::TyConId, crate::kinds::Kind)>,
    pub type_kinds: std::collections::BTreeMap<u32, crate::kinds::Kind>,
    pub constructors: Vec<ConDecl>,
    pub locations: Vec<(Span, String)>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Case(scrut, binder, ty, alts) => {
                write!(f, "(case {scrut} as {binder} -> {ty}")?;
                for alt in alts {
                    write!(f, " [ {:?}", alt.con)?;
                    for id in &alt.binders {
                        write!(f, " {id}")?;
                    }
                    write!(f, " -> {} ]", alt.rhs)?;
                }
                write!(f, ")")
            }
            Self::Join(id, params, body, rest) => {
                write!(f, "(join {id} (")?;
                for p in params {
                    write!(f, " {p}")?;
                }
                write!(f, ") = {body} in {rest})")
            }
            Self::Jump(id, args, ty) => {
                write!(f, "(jump {id} : {ty}")?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::Con(id, types, args) => {
                write!(f, "(con {}", id.name)?;
                for t in types {
                    write!(f, " [{t}]")?;
                }
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Match(m) => {
                write!(f, "(match")?;
                for s in &m.scruts {
                    write!(f, " {}", s.name)?;
                }
                for row in &m.rows {
                    write!(f, " {row}")?;
                }
                write!(f, ")")
            }
            Self::PatternFail(_, ty) => write!(f, "(pattern-fail : {ty})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double(bits) => write!(
                f,
                "{}",
                crate::primitives::show_double(f64::from_bits(*bits))
            ),
            Self::Int(n) => write!(f, "{n}"),
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// Constructor typing is immutable and contains no inference metavariables.
#[derive(Clone, Debug)]
pub struct ConDecl {
    pub labels: Option<Vec<String>>,
    pub id: crate::data::ConId,
    pub tag: u32,
    pub parameters: Vec<u32>,
    pub fields: Vec<Type>,
    pub result: Type,
}
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}
/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}
/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}
/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
impl std::fmt::Display for Pat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(id) => write!(f, "{}", id.name),
            Self::Wild => write!(f, "_"),
            Self::As(id, p) => write!(f, "{}@{p}", id.name),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Con(id, ps) => {
                write!(f, "({}", id.name)?;
                for p in ps {
                    write!(f, " {p}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Row {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for p in &self.pats {
            write!(f, "{p} ")?;
        }
        for local in &self.locals {
            write!(f, "{local} ")?;
        }
        match &self.body {
            RowBody::Plain(e) => write!(f, "-> {e}")?,
            RowBody::Guarded(gs) => {
                for (g, e) in gs {
                    write!(f, "| {g} -> {e} ")?;
                }
            }
        }
        write!(f, " ]")
    }
}
/// Type abstractions erase; a constrained value is an ordinary dictionary function.
pub fn is_function(mut e: &Expr) -> bool {
    while let Expr::TyLam(_, body) = e {
        e = body;
    }
    matches!(e, Expr::Lam(..))
}
/// Dictionary calls introduce global dependencies absent from the source graph.
pub fn order(groups: Vec<BindGroup>) -> Vec<BindGroup> {
    fn refs(e: &Expr, out: &mut std::collections::BTreeSet<u32>) {
        match e {
            Expr::Case(s, _, _, alts) => {
                refs(s, out);
                for a in alts {
                    refs(&a.rhs, out)
                }
            }
            Expr::Join(_, _, b, r) => {
                refs(b, out);
                refs(r, out)
            }
            Expr::Jump(_, args, _) => {
                for a in args {
                    refs(a, out)
                }
            }
            Expr::Global(id) => {
                out.insert(id.unique());
            }
            Expr::Lam(_, b) | Expr::TyLam(_, b) | Expr::TyApp(b, _) => {
                refs(b, out)
            }
            Expr::App(a, b) => {
                refs(a, out);
                refs(b, out);
            }
            Expr::Let(g, b) => {
                for bind in &g.binds {
                    refs(&bind.rhs, out);
                }
                refs(b, out);
            }
            Expr::Con(_, _, args) | Expr::Prim(_, _, args, _) => {
                for a in args {
                    refs(a, out);
                }
            }
            Expr::Match(m) => {
                for row in &m.rows {
                    for g in &row.locals {
                        for b in &g.binds {
                            refs(&b.rhs, out);
                        }
                    }
                    match &row.body {
                        RowBody::Plain(e) => refs(e, out),
                        RowBody::Guarded(gs) => {
                            for (g, e) in gs {
                                refs(g, out);
                                refs(e, out);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
    let binds =
        groups.into_iter().flat_map(|g| g.binds).collect::<Vec<_>>();
    let indices = binds
        .iter()
        .enumerate()
        .map(|(i, b)| (b.name.unique(), i))
        .collect::<std::collections::BTreeMap<_, _>>();
    let edges = binds
        .iter()
        .map(|b| {
            let mut names = std::collections::BTreeSet::new();
            refs(&b.rhs, &mut names);
            names
                .into_iter()
                .filter_map(|n| indices.get(&n).copied())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut binds = binds.into_iter().map(Some).collect::<Vec<_>>();
    crate::scc::components(&edges)
        .into_iter()
        .map(|xs| BindGroup {
            recursive: (xs.len() > 1 || edges[xs[0]].contains(&xs[0]))
                && xs
                    .iter()
                    .all(|i| is_function(&binds[*i].as_ref().unwrap().rhs)),
            binds: xs
                .into_iter()
                .map(|i| binds[i].take().unwrap())
                .collect(),
        })
        .collect()
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
pub use crate::core_type::Type;
use crate::{ast::Op, source::Span};
/// A Core identifier carries its independently checkable type.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Id {
    pub name: crate::names::Id,
    pub ty: Type,
}
impl std::fmt::Display for Id {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} : {}", self.name, self.ty)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Immediate source values; strings are primitive UTF-8 values, not character lists.
pub enum Lit {
    Double(u64),
    Int(i64),
    Char(char),
    String(String),
}
#[derive(Clone, Debug)]
/// System F Core makes type abstraction and instantiation explicit.
pub enum Expr {
    Var(Id),
    Global(Id),
    TyLam(u32, Box<Expr>),
    TyApp(Box<Expr>, Type),
    Lit(Lit),
    Lam(Id, Box<Expr>),
    App(Box<Expr>, Box<Expr>),
    Let(BindGroup, Box<Expr>),
    Con(crate::data::ConId, Vec<Type>, Vec<Expr>),
    Match(Match),
    Case(Box<Expr>, Id, Type, Vec<Alt>),
    Join(crate::names::Id, Vec<Id>, Box<Expr>, Box<Expr>),
    Jump(crate::names::Id, Vec<Expr>, Type),
    PatternFail(Span, Type),
    Prim(Op, Vec<Type>, Vec<Expr>, Span),
}
/// Flat alternatives inspect one constructor or literal, never nested patterns.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AltCon {
    Con(crate::data::ConId),
    Lit(Lit),
    Default,
}
/// Constructor slots become typed binders scoped over the alternative body.
#[derive(Clone, Debug)]
pub struct Alt {
    pub con: AltCon,
    pub binders: Vec<Id>,
    pub rhs: Expr,
}
#[derive(Clone, Debug)]
/// A recursive binding is guaranteed to have a lambda right-hand side.
pub struct Bind {
    pub name: Id,
    pub rhs: Box<Expr>,
}
#[derive(Clone, Debug)]
/// Globals are initialized in dependency order, with explicit recursive components.
pub struct Program {
    pub groups: Vec<BindGroup>,
    /// Flat type fingerprints survive erasure for cross-module ABI checks.
    pub global_types:
        std::collections::BTreeMap<crate::names::Id, Vec<String>>,
    pub main: Id,
    pub data_types: Vec<(crate::data::TyConId, crate::kinds::Kind)>,
    pub type_kinds: std::collections::BTreeMap<u32, crate::kinds::Kind>,
    pub constructors: Vec<ConDecl>,
    pub locations: Vec<(Span, String)>,
}
impl std::fmt::Display for Expr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Case(scrut, binder, ty, alts) => {
                write!(f, "(case {scrut} as {binder} -> {ty}")?;
                for alt in alts {
                    write!(f, " [ {:?}", alt.con)?;
                    for id in &alt.binders {
                        write!(f, " {id}")?;
                    }
                    write!(f, " -> {} ]", alt.rhs)?;
                }
                write!(f, ")")
            }
            Self::Join(id, params, body, rest) => {
                write!(f, "(join {id} (")?;
                for p in params {
                    write!(f, " {p}")?;
                }
                write!(f, ") = {body} in {rest})")
            }
            Self::Jump(id, args, ty) => {
                write!(f, "(jump {id} : {ty}")?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Var(n) => write!(f, "{}", n.name),
            Self::Global(n) => write!(f, "@{}", n.name),
            Self::TyLam(v, b) => write!(f, "(type-lambda t{v} {b})"),
            Self::TyApp(a, t) => write!(f, "(type-app {a} {t})"),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Lam(n, b) => write!(f, "(lambda {n} {b})"),
            Self::App(a, b) => write!(f, "(app {a} {b})"),
            Self::Let(group, body) => write!(f, "(let {group} in {body})"),
            Self::Con(id, types, args) => {
                write!(f, "(con {}", id.name)?;
                for t in types {
                    write!(f, " [{t}]")?;
                }
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
            Self::Match(m) => {
                write!(f, "(match")?;
                for s in &m.scruts {
                    write!(f, " {}", s.name)?;
                }
                for row in &m.rows {
                    write!(f, " {row}")?;
                }
                write!(f, ")")
            }
            Self::PatternFail(_, ty) => write!(f, "(pattern-fail : {ty})"),
            Self::Prim(op, _, args, _) => {
                write!(f, "(prim {}", op.text())?;
                for a in args {
                    write!(f, " {a}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Lit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Double(bits) => write!(
                f,
                "{}",
                crate::primitives::show_double(f64::from_bits(*bits))
            ),
            Self::Int(n) => write!(f, "{n}"),
            Self::Char(c) => {
                write!(f, "{}", crate::ast::quote(&c.to_string(), '\''))
            }
            Self::String(s) => write!(f, "{}", crate::ast::quote(s, '"')),
        }
    }
}
impl std::fmt::Display for BindGroup {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{} {{", if self.recursive { "rec" } else { "nonrec" })?;
        for b in &self.binds {
            write!(f, " {} = {};", b.name, b.rhs)?;
        }
        write!(f, " }}")
    }
}
/// Independently typecheck System F Core after each Core-producing pass.
pub fn lint(program: &Program) -> Result<(), String> {
    crate::core_lint::lint(program)
}
impl Id {
    pub fn unique(&self) -> u32 {
        self.name.name.unique
    }
}
/// Constructor typing is immutable and contains no inference metavariables.
#[derive(Clone, Debug)]
pub struct ConDecl {
    pub labels: Option<Vec<String>>,
    pub id: crate::data::ConId,
    pub tag: u32,
    pub parameters: Vec<u32>,
    pub fields: Vec<Type>,
    pub result: Type,
}
/// Nested patterns remain until the decision-tree compiler in Chapter 16.
#[derive(Clone, Debug)]
pub enum Pat {
    Var(Id),
    Wild,
    As(Id, Box<Pat>),
    Con(crate::data::ConId, Vec<Pat>),
    Lit(Lit),
}
/// A guarded row falls through when all its Boolean tests fail.
#[derive(Clone, Debug)]
pub enum RowBody {
    Plain(Box<Expr>),
    Guarded(Vec<(Expr, Expr)>),
}
/// Row-local declarations run after patterns match and are shared by the guards.
#[derive(Clone, Debug)]
pub struct Row {
    pub pats: Vec<Pat>,
    pub locals: Vec<BindGroup>,
    pub body: RowBody,
    pub span: Span,
}
/// Scrutinees name strict preceding lets; the match records its result type and failure site.
#[derive(Clone, Debug)]
pub struct Match {
    pub scruts: Vec<Id>,
    pub rows: Vec<Row>,
    pub result: Type,
    pub span: Span,
}
impl std::fmt::Display for Pat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(id) => write!(f, "{}", id.name),
            Self::Wild => write!(f, "_"),
            Self::As(id, p) => write!(f, "{}@{p}", id.name),
            Self::Lit(l) => write!(f, "{l}"),
            Self::Con(id, ps) => {
                write!(f, "({}", id.name)?;
                for p in ps {
                    write!(f, " {p}")?;
                }
                write!(f, ")")
            }
        }
    }
}
impl std::fmt::Display for Row {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[ ")?;
        for p in &self.pats {
            write!(f, "{p} ")?;
        }
        for local in &self.locals {
            write!(f, "{local} ")?;
        }
        match &self.body {
            RowBody::Plain(e) => write!(f, "-> {e}")?,
            RowBody::Guarded(gs) => {
                for (g, e) in gs {
                    write!(f, "| {g} -> {e} ")?;
                }
            }
        }
        write!(f, " ]")
    }
}
/// Type abstractions erase; a constrained value is an ordinary dictionary function.
pub fn is_function(mut e: &Expr) -> bool {
    while let Expr::TyLam(_, body) = e {
        e = body;
    }
    matches!(e, Expr::Lam(..))
}
/// Dictionary calls introduce global dependencies absent from the source graph.
pub fn order(groups: Vec<BindGroup>) -> Vec<BindGroup> {
    fn refs(e: &Expr, out: &mut std::collections::BTreeSet<u32>) {
        match e {
            Expr::Case(s, _, _, alts) => {
                refs(s, out);
                for a in alts {
                    refs(&a.rhs, out)
                }
            }
            Expr::Join(_, _, b, r) => {
                refs(b, out);
                refs(r, out)
            }
            Expr::Jump(_, args, _) => {
                for a in args {
                    refs(a, out)
                }
            }
            Expr::Global(id) => {
                out.insert(id.unique());
            }
            Expr::Lam(_, b) | Expr::TyLam(_, b) | Expr::TyApp(b, _) => {
                refs(b, out)
            }
            Expr::App(a, b) => {
                refs(a, out);
                refs(b, out);
            }
            Expr::Let(g, b) => {
                for bind in &g.binds {
                    refs(&bind.rhs, out);
                }
                refs(b, out);
            }
            Expr::Con(_, _, args) | Expr::Prim(_, _, args, _) => {
                for a in args {
                    refs(a, out);
                }
            }
            Expr::Match(m) => {
                for row in &m.rows {
                    for g in &row.locals {
                        for b in &g.binds {
                            refs(&b.rhs, out);
                        }
                    }
                    match &row.body {
                        RowBody::Plain(e) => refs(e, out),
                        RowBody::Guarded(gs) => {
                            for (g, e) in gs {
                                refs(g, out);
                                refs(e, out);
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
    let binds =
        groups.into_iter().flat_map(|g| g.binds).collect::<Vec<_>>();
    let indices = binds
        .iter()
        .enumerate()
        .map(|(i, b)| (b.name.unique(), i))
        .collect::<std::collections::BTreeMap<_, _>>();
    let edges = binds
        .iter()
        .map(|b| {
            let mut names = std::collections::BTreeSet::new();
            refs(&b.rhs, &mut names);
            names
                .into_iter()
                .filter_map(|n| indices.get(&n).copied())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    let mut binds = binds.into_iter().map(Some).collect::<Vec<_>>();
    crate::scc::components(&edges)
        .into_iter()
        .map(|xs| BindGroup {
            recursive: (xs.len() > 1 || edges[xs[0]].contains(&xs[0]))
                && xs
                    .iter()
                    .all(|i| is_function(&binds[*i].as_ref().unwrap().rhs)),
            binds: xs
                .into_iter()
                .map(|i| binds[i].take().unwrap())
                .collect(),
        })
        .collect()
}
#[derive(Clone, Debug)]
/// An ordered component; recursive members are in scope in every right-hand side.
pub struct BindGroup {
    pub recursive: bool,
    pub binds: Vec<Bind>,
}
}

재귀 그룹은 함수를 담습니다. 비재귀 그룹의 우변은 의존성 순서대로 평가됩니다. Case 바인더는 이미 평가된 검사 대상에 이름을 붙입니다. 패턴 실패와 실패하는 프리미티브는 소스 위치를 보존합니다.

A-정규형

아톰은 스코프가 올바른 환경에서 읽을 때 효과를 수행하거나 실패할 수 없습니다. Let은 하나의 엄격한 계산에 이름을 붙입니다. 본문 타입은 Rc<Anf>이므로, 머신과 변환 작업 목록이 불변 구문을 공유할 수 있습니다.

#![allow(unused)]
fn main() {
use crate::{
    ast::Op,
    core::{self, AltCon, Lit},
    data::ConId,
    erase,
    names::Id,
    session::Session,
    source::Span,
};
use std::{collections::BTreeMap, rc::Rc};
/// An erased binder keeps its unique identity and diagnostic spelling.
pub type Var = Id;
/// Syntax is immutable and shared between closures and machine states.
pub type Body = Rc<Anf>;
#[derive(Clone, Debug)]
/// Lambda parameters are merged only across syntactically adjacent lambdas.
pub struct Lambda {
    pub params: Vec<Var>,
    pub body: Body,
}
#[derive(Clone, Debug)]
/// One strict computation whose operands have already been evaluated.
pub enum Rhs {
    Atom(Atom),
    Call(Atom, Vec<Atom>),
    Con(ConId, Vec<Atom>),
    Prim(Op, Vec<Atom>, Span),
    Lambda(Lambda),
    Field(Atom, u32),
}
#[derive(Clone, Debug)]
/// Jumps transfer within a function; tail calls replace its activation.
pub enum Anf {
    Let(Var, Rhs, Body),
    LetRec(Vec<(Var, Lambda)>, Body),
    Join(Id, Vec<Var>, Body, Body),
    Jump(Id, Vec<Atom>),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Ret(Atom),
    TailCall(Atom, Vec<Atom>),
    Fail(Span),
}
#[derive(Clone, Debug)]
/// Global initialization order is preserved from typed Core.
pub struct Program {
    pub globals: Vec<(Id, Body)>,
    pub metadata: core::Program,
    pub reps: BTreeMap<Var, crate::rep::Rep>,
}
#[derive(Clone)]
enum Context {
    Return,
    Jump(Id),
    Bind(Var, Body),
}
struct Convert<'a> {
    sess: &'a mut Session,
    arities: BTreeMap<Id, usize>,
    reps: BTreeMap<Var, crate::rep::Rep>,
}
fn arity(mut e: &erase::Expr) -> usize {
    let mut n = 0;
    while let erase::Node::Lam(_, b) = &e.node {
        n += 1;
        e = b;
    }
    n
}
fn collect(e: &erase::Expr, out: &mut BTreeMap<Id, usize>) {
    use erase::Node as E;
    let mut todo = vec![e];
    while let Some(e) = todo.pop() {
        match &e.node {
            E::Let(_, bs, b) => {
                for (v, e) in bs {
                    let n = arity(e);
                    if n > 0 {
                        out.insert(v.clone(), n);
                    }
                    todo.push(e);
                }
                todo.push(b);
            }
            E::Lam(_, b) => todo.push(b),
            E::App(a, b) | E::Join(_, _, a, b) => {
                todo.push(a);
                todo.push(b);
            }
            E::Con(_, es) | E::Prim(_, es, _) | E::Jump(_, es) => {
                todo.extend(es)
            }
            E::Case(e, _, alts) => {
                todo.push(e);
                todo.extend(alts.iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
impl Convert<'_> {
    fn fresh(&mut self, rep: crate::rep::Rep) -> Var {
        let id = self.sess.fresh_id("$a");
        self.reps.insert(id.clone(), rep);
        id
    }
    fn finish(&mut self, a: Atom, k: Context) -> Body {
        Rc::new(match k {
            Context::Return => Anf::Ret(a),
            Context::Jump(j) => Anf::Jump(j, vec![a]),
            Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
        })
    }
    fn rhs(&mut self, r: Rhs, k: Context, rep: crate::rep::Rep) -> Body {
        if let Context::Bind(v, b) = k {
            return Rc::new(Anf::Let(v, r, b));
        }
        if let (Rhs::Call(f, args), Context::Return) = (&r, &k) {
            return Rc::new(Anf::TailCall(f.clone(), args.clone()));
        }
        let v = self.fresh(rep);
        let rest = self.finish(Atom::Var(v.clone()), k);
        Rc::new(Anf::Let(v, r, rest))
    }
    /// Tasks hold pending compiler work. Neither expression depth nor the
    /// number of operands adds a Rust call frame.
    fn expr(&mut self, e: erase::Expr, k: Context) -> Body {
        use crate::rep::Rep;
        use erase::Node as E;
        enum Work {
            Expr(erase::Expr, Context),
            Bindings(Vec<(Var, erase::Expr)>),
            Lambda(Vec<Var>, Context, Rep),
            Rec(Vec<(Var, Vec<Var>)>),
            Case(
                erase::Expr,
                Var,
                Vec<(AltCon, Vec<Var>)>,
                Option<(Id, Var, Body)>,
            ),
            Join(Id, Vec<Var>, Option<(Id, Var, Body)>),
            Call(erase::Expr, Var, Vec<erase::Expr>, Var, Rep),
        }
        fn peel(mut e: erase::Expr) -> (Vec<Var>, erase::Expr) {
            let mut ps = vec![];
            while let E::Lam(v, b) = e.node {
                ps.push(v);
                e = *b;
            }
            (ps, e)
        }
        fn branch(
            k: Context,
            sess: &mut Session,
        ) -> (Context, Option<(Id, Var, Body)>) {
            match k {
                Context::Bind(v, b) => {
                    let j = sess.fresh_id("$cont");
                    (Context::Jump(j.clone()), Some((j, v, b)))
                }
                k => (k, None),
            }
        }
        fn wrap(body: Body, cont: Option<(Id, Var, Body)>) -> Body {
            match cont {
                Some((j, v, b)) => Rc::new(Anf::Join(j, vec![v], b, body)),
                None => body,
            }
        }
        let mut todo = vec![Work::Expr(e, k)];
        let mut results: Vec<Body> = vec![];
        while let Some(work) = todo.pop() {
            match work {
                Work::Bindings(mut bs) => {
                    if let Some((v, e)) = bs.pop() {
                        let b = results.pop().unwrap();
                        todo.push(Work::Bindings(bs));
                        todo.push(Work::Expr(e, Context::Bind(v, b)));
                    }
                }
                Work::Lambda(params, k, rep) => {
                    let body = results.pop().unwrap();
                    results.push(self.rhs(
                        Rhs::Lambda(Lambda { params, body }),
                        k,
                        rep,
                    ));
                }
                Work::Rec(bs) => {
                    let bodies =
                        results.split_off(results.len() - bs.len());
                    let rest = results.pop().unwrap();
                    let bs = bs
                        .into_iter()
                        .zip(bodies)
                        .map(|((v, params), body)| {
                            (v, Lambda { params, body })
                        })
                        .collect();
                    results.push(Rc::new(Anf::LetRec(bs, rest)));
                }
                Work::Case(e, v, heads, cont) => {
                    let bodies =
                        results.split_off(results.len() - heads.len());
                    let alts = heads
                        .into_iter()
                        .zip(bodies)
                        .map(|((c, ps), b)| (c, ps, b))
                        .collect();
                    let b = wrap(
                        Rc::new(Anf::Case(Atom::Var(v.clone()), alts)),
                        cont,
                    );
                    todo.push(Work::Expr(e, Context::Bind(v, b)));
                }
                Work::Join(j, ps, cont) => {
                    let rest = results.pop().unwrap();
                    let body = results.pop().unwrap();
                    results.push(wrap(
                        Rc::new(Anf::Join(j, ps, body, rest)),
                        cont,
                    ));
                }
                Work::Call(head, f, args, result, rep) => {
                    let rest = results.pop().unwrap();
                    let vars: Vec<_> =
                        args.iter().map(|e| self.fresh(e.rep)).collect();
                    let atoms =
                        vars.iter().cloned().map(Atom::Var).collect();
                    let b = self.rhs(
                        Rhs::Call(Atom::Var(f.clone()), atoms),
                        Context::Bind(result, rest),
                        rep,
                    );
                    let bs = std::iter::once((f, head))
                        .chain(vars.into_iter().zip(args))
                        .collect();
                    results.push(b);
                    todo.push(Work::Bindings(bs));
                }
                Work::Expr(e, k) => {
                    let rep = e.rep;
                    match e.node {
                        E::Var(v) => {
                            results.push(self.finish(Atom::Var(v), k))
                        }
                        E::Global(v) => {
                            results.push(self.finish(Atom::Global(v), k))
                        }
                        E::Lit(l) => {
                            results.push(self.finish(Atom::Lit(l), k))
                        }
                        node @ E::Lam(..) => {
                            let (ps, body) =
                                peel(erase::Expr { node, rep });
                            todo.push(Work::Lambda(ps, k, rep));
                            todo.push(Work::Expr(body, Context::Return));
                        }
                        node @ E::App(..) => {
                            let mut head = erase::Expr { node, rep };
                            let mut args = vec![];
                            let mut reps = vec![];
                            while let E::App(f, a) = head.node {
                                reps.push(head.rep);
                                args.push(*a);
                                head = *f;
                            }
                            args.reverse();
                            reps.reverse();
                            let known = match &head.node {
                                E::Var(v) | E::Global(v) => {
                                    self.arities.get(v).copied()
                                }
                                E::Lam(..) => Some(arity(&head)),
                                _ => None,
                            };
                            // Unknown calls execute before evaluating their next argument.
                            let n = known.unwrap_or(1).min(args.len());
                            let extra = args.split_off(n);
                            let extra_reps = reps.split_off(n);
                            let f = self.fresh(head.rep);
                            if extra.is_empty() {
                                let vars: Vec<_> = args
                                    .iter()
                                    .map(|e| self.fresh(e.rep))
                                    .collect();
                                let atoms = vars
                                    .iter()
                                    .cloned()
                                    .map(Atom::Var)
                                    .collect();
                                results.push(self.rhs(
                                    Rhs::Call(Atom::Var(f.clone()), atoms),
                                    k,
                                    rep,
                                ));
                                todo.push(Work::Bindings(
                                    std::iter::once((f, head))
                                        .chain(vars.into_iter().zip(args))
                                        .collect(),
                                ));
                            } else {
                                let result = self.fresh(Rep::Word);
                                let application =
                                    extra.into_iter().zip(extra_reps).fold(
                                        erase::Expr {
                                            node: E::Var(result.clone()),
                                            rep: Rep::Word,
                                        },
                                        |f, (a, rep)| erase::Expr {
                                            node: E::App(
                                                Box::new(f),
                                                Box::new(a),
                                            ),
                                            rep,
                                        },
                                    );
                                todo.push(Work::Call(
                                    head,
                                    f,
                                    args,
                                    result,
                                    Rep::Word,
                                ));
                                todo.push(Work::Expr(application, k));
                            }
                        }
                        E::Let(rec, bs, b) => {
                            if rec {
                                let (heads, bodies): (Vec<_>, Vec<_>) = bs
                                    .into_iter()
                                    .map(|(v, e)| {
                                        let (ps, e) = peel(e);
                                        ((v, ps), e)
                                    })
                                    .unzip();
                                todo.push(Work::Rec(heads));
                                todo.extend(bodies.into_iter().rev().map(
                                    |e| Work::Expr(e, Context::Return),
                                ));
                            } else {
                                todo.push(Work::Bindings(bs));
                            }
                            todo.push(Work::Expr(*b, k));
                        }
                        node @ (E::Con(..) | E::Prim(..) | E::Jump(..)) => {
                            let args = match &node {
                                E::Con(_, es)
                                | E::Prim(_, es, _)
                                | E::Jump(_, es) => es,
                                _ => unreachable!(),
                            };
                            let vars: Vec<_> = args
                                .iter()
                                .map(|e| self.fresh(e.rep))
                                .collect();
                            let atoms = vars
                                .iter()
                                .cloned()
                                .map(Atom::Var)
                                .collect();
                            let (args, body) = match node {
                                E::Con(c, es) => (
                                    es,
                                    self.rhs(Rhs::Con(c, atoms), k, rep),
                                ),
                                E::Prim(p, es, s) => (
                                    es,
                                    self.rhs(
                                        Rhs::Prim(p, atoms, s),
                                        k,
                                        rep,
                                    ),
                                ),
                                E::Jump(j, es) => {
                                    (es, Rc::new(Anf::Jump(j, atoms)))
                                }
                                _ => unreachable!(),
                            };
                            results.push(body);
                            todo.push(Work::Bindings(
                                vars.into_iter().zip(args).collect(),
                            ));
                        }
                        E::Case(e, v, alts) => {
                            let (k, cont) = branch(k, self.sess);
                            let (heads, bodies): (Vec<_>, Vec<_>) = alts
                                .into_iter()
                                .map(|(c, ps, b)| ((c, ps), b))
                                .unzip();
                            todo.push(Work::Case(*e, v, heads, cont));
                            todo.extend(
                                bodies
                                    .into_iter()
                                    .rev()
                                    .map(|e| Work::Expr(e, k.clone())),
                            );
                        }
                        E::Join(j, ps, b, r) => {
                            let (k, cont) = branch(k, self.sess);
                            todo.push(Work::Join(j, ps, cont));
                            todo.push(Work::Expr(*r, k.clone()));
                            todo.push(Work::Expr(*b, k));
                        }
                        E::Fail(s) => results.push(Rc::new(Anf::Fail(s))),
                    }
                }
            }
        }
        assert_eq!(results.len(), 1);
        results.pop().unwrap()
    }
}
/// Normalize left-to-right, sharing every non-tail continuation exactly once.
pub fn convert(sess: &mut Session, program: erase::Program) -> Program {
    let mut arities = BTreeMap::new();
    for (v, e) in &program.globals {
        if arity(e) > 0 {
            arities.insert(v.clone(), arity(e));
        }
        collect(e, &mut arities);
    }
    let mut c = Convert {
        sess,
        arities,
        reps: program.reps,
    };
    let globals = program
        .globals
        .into_iter()
        .map(|(v, e)| (v, c.expr(e, Context::Return)))
        .collect();
    Program {
        reps: c.reps,
        globals,
        metadata: program.metadata,
    }
}
impl std::fmt::Display for Atom {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(v) => write!(f, "{v}"),
            Self::Global(v) => write!(f, "@{v}"),
            Self::Lit(l) => write!(f, "{l}"),
        }
    }
}
fn list<T: std::fmt::Display>(items: &[T]) -> String {
    items
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::Call(a, args) => write!(f, "call {a}({})", list(args)),
            Self::Con(c, args) => write!(f, "{}({})", c.name, list(args)),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::Lambda(l) => {
                write!(f, "lambda ({}) {{ {} }}", list(&l.params), l.body)
            }
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
        }
    }
}
impl std::fmt::Display for Anf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Body(&'a Anf),
            Rhs(&'a Rhs),
            Text(String),
        }
        let mut todo = vec![Work::Body(self)];
        while let Some(work) = todo.pop() {
            let mut parts = vec![];
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Rhs(Rhs::Lambda(l)) => {
                    write!(f, "lambda ({}) {{ ", list(&l.params))?;
                    parts.extend([
                        Work::Body(&l.body),
                        Work::Text(" }".into()),
                    ]);
                }
                Work::Rhs(r) => write!(f, "{r}")?,
                Work::Body(b) => match b {
                    Self::Let(v, r, b) => {
                        write!(f, "let {v} = ")?;
                        parts.extend([
                            Work::Rhs(r),
                            Work::Text(";\n".into()),
                            Work::Body(b),
                        ]);
                    }
                    Self::LetRec(bs, b) => {
                        for (v, l) in bs {
                            parts.extend([
                                Work::Text(format!(
                                    "rec {v}({}) = {{ ",
                                    list(&l.params)
                                )),
                                Work::Body(&l.body),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Body(b));
                    }
                    Self::Join(j, ps, b, r) => {
                        write!(f, "join {j}({}) = {{ ", list(ps))?;
                        parts.extend([
                            Work::Body(b),
                            Work::Text(" };\n".into()),
                            Work::Body(r),
                        ]);
                    }
                    Self::Jump(j, args) => {
                        write!(f, "jump {j}({})", list(args))?
                    }
                    Self::Case(a, alts) => {
                        writeln!(f, "case {a} {{")?;
                        for (c, ps, b) in alts {
                            let head = match c {
                                AltCon::Con(c) => c.name.clone(),
                                AltCon::Lit(l) => l.to_string(),
                                AltCon::Default => "_".into(),
                            };
                            parts.extend([
                                Work::Text(format!(
                                    "{head}({}) -> {{ ",
                                    list(ps)
                                )),
                                Work::Body(b),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Text("}".into()));
                    }
                    Self::Ret(a) => write!(f, "return {a}")?,
                    Self::TailCall(fun, args) => {
                        write!(f, "tail {fun}({})", list(args))?
                    }
                    Self::Fail(s) => write!(f, "fail {s:?}")?,
                },
            }
            todo.extend(parts.into_iter().rev());
        }
        Ok(())
    }
}
/// Drain child edges explicitly when the last owner releases a deep ANF tree.
impl Drop for Anf {
    fn drop(&mut self) {
        fn edges(node: &mut Anf, todo: &mut Vec<Body>) {
            fn take(b: &mut Body) -> Body {
                std::mem::replace(
                    b,
                    Rc::new(Anf::Ret(Atom::Lit(Lit::Int(0)))),
                )
            }
            match node {
                Anf::Let(_, r, b) => {
                    todo.push(take(b));
                    if let Rhs::Lambda(l) = r {
                        todo.push(take(&mut l.body));
                    }
                }
                Anf::LetRec(bs, b) => {
                    todo.push(take(b));
                    todo.extend(bs.drain(..).map(|(_, l)| l.body));
                }
                Anf::Join(_, _, b, r) => {
                    todo.push(take(b));
                    todo.push(take(r));
                }
                Anf::Case(_, alts) => {
                    todo.extend(alts.drain(..).map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        edges(self, &mut todo);
        while let Some(b) = todo.pop() {
            if let Ok(mut node) = Rc::try_unwrap(b) {
                edges(&mut node, &mut todo);
                // Empty this node before Drop runs again on the local variable.
                // Its remaining children are only leaf placeholders.
            }
        }
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
/// Atoms cannot perform work or fail when read from a well-scoped environment.
pub enum Atom {
    Var(Var),
    Global(Id),
    Lit(Lit),
}
use crate::{
    ast::Op,
    core::{self, AltCon, Lit},
    data::ConId,
    erase,
    names::Id,
    session::Session,
    source::Span,
};
use std::{collections::BTreeMap, rc::Rc};
/// An erased binder keeps its unique identity and diagnostic spelling.
pub type Var = Id;
/// Syntax is immutable and shared between closures and machine states.
pub type Body = Rc<Anf>;
#[derive(Clone, Debug, PartialEq, Eq)]
/// Atoms cannot perform work or fail when read from a well-scoped environment.
pub enum Atom {
    Var(Var),
    Global(Id),
    Lit(Lit),
}
#[derive(Clone, Debug)]
/// One strict computation whose operands have already been evaluated.
pub enum Rhs {
    Atom(Atom),
    Call(Atom, Vec<Atom>),
    Con(ConId, Vec<Atom>),
    Prim(Op, Vec<Atom>, Span),
    Lambda(Lambda),
    Field(Atom, u32),
}
#[derive(Clone, Debug)]
/// Jumps transfer within a function; tail calls replace its activation.
pub enum Anf {
    Let(Var, Rhs, Body),
    LetRec(Vec<(Var, Lambda)>, Body),
    Join(Id, Vec<Var>, Body, Body),
    Jump(Id, Vec<Atom>),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Ret(Atom),
    TailCall(Atom, Vec<Atom>),
    Fail(Span),
}
#[derive(Clone, Debug)]
/// Global initialization order is preserved from typed Core.
pub struct Program {
    pub globals: Vec<(Id, Body)>,
    pub metadata: core::Program,
    pub reps: BTreeMap<Var, crate::rep::Rep>,
}
#[derive(Clone)]
enum Context {
    Return,
    Jump(Id),
    Bind(Var, Body),
}
struct Convert<'a> {
    sess: &'a mut Session,
    arities: BTreeMap<Id, usize>,
    reps: BTreeMap<Var, crate::rep::Rep>,
}
fn arity(mut e: &erase::Expr) -> usize {
    let mut n = 0;
    while let erase::Node::Lam(_, b) = &e.node {
        n += 1;
        e = b;
    }
    n
}
fn collect(e: &erase::Expr, out: &mut BTreeMap<Id, usize>) {
    use erase::Node as E;
    let mut todo = vec![e];
    while let Some(e) = todo.pop() {
        match &e.node {
            E::Let(_, bs, b) => {
                for (v, e) in bs {
                    let n = arity(e);
                    if n > 0 {
                        out.insert(v.clone(), n);
                    }
                    todo.push(e);
                }
                todo.push(b);
            }
            E::Lam(_, b) => todo.push(b),
            E::App(a, b) | E::Join(_, _, a, b) => {
                todo.push(a);
                todo.push(b);
            }
            E::Con(_, es) | E::Prim(_, es, _) | E::Jump(_, es) => {
                todo.extend(es)
            }
            E::Case(e, _, alts) => {
                todo.push(e);
                todo.extend(alts.iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
impl Convert<'_> {
    fn fresh(&mut self, rep: crate::rep::Rep) -> Var {
        let id = self.sess.fresh_id("$a");
        self.reps.insert(id.clone(), rep);
        id
    }
    fn finish(&mut self, a: Atom, k: Context) -> Body {
        Rc::new(match k {
            Context::Return => Anf::Ret(a),
            Context::Jump(j) => Anf::Jump(j, vec![a]),
            Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
        })
    }
    fn rhs(&mut self, r: Rhs, k: Context, rep: crate::rep::Rep) -> Body {
        if let Context::Bind(v, b) = k {
            return Rc::new(Anf::Let(v, r, b));
        }
        if let (Rhs::Call(f, args), Context::Return) = (&r, &k) {
            return Rc::new(Anf::TailCall(f.clone(), args.clone()));
        }
        let v = self.fresh(rep);
        let rest = self.finish(Atom::Var(v.clone()), k);
        Rc::new(Anf::Let(v, r, rest))
    }
    /// Tasks hold pending compiler work. Neither expression depth nor the
    /// number of operands adds a Rust call frame.
    fn expr(&mut self, e: erase::Expr, k: Context) -> Body {
        use crate::rep::Rep;
        use erase::Node as E;
        enum Work {
            Expr(erase::Expr, Context),
            Bindings(Vec<(Var, erase::Expr)>),
            Lambda(Vec<Var>, Context, Rep),
            Rec(Vec<(Var, Vec<Var>)>),
            Case(
                erase::Expr,
                Var,
                Vec<(AltCon, Vec<Var>)>,
                Option<(Id, Var, Body)>,
            ),
            Join(Id, Vec<Var>, Option<(Id, Var, Body)>),
            Call(erase::Expr, Var, Vec<erase::Expr>, Var, Rep),
        }
        fn peel(mut e: erase::Expr) -> (Vec<Var>, erase::Expr) {
            let mut ps = vec![];
            while let E::Lam(v, b) = e.node {
                ps.push(v);
                e = *b;
            }
            (ps, e)
        }
        fn branch(
            k: Context,
            sess: &mut Session,
        ) -> (Context, Option<(Id, Var, Body)>) {
            match k {
                Context::Bind(v, b) => {
                    let j = sess.fresh_id("$cont");
                    (Context::Jump(j.clone()), Some((j, v, b)))
                }
                k => (k, None),
            }
        }
        fn wrap(body: Body, cont: Option<(Id, Var, Body)>) -> Body {
            match cont {
                Some((j, v, b)) => Rc::new(Anf::Join(j, vec![v], b, body)),
                None => body,
            }
        }
        let mut todo = vec![Work::Expr(e, k)];
        let mut results: Vec<Body> = vec![];
        while let Some(work) = todo.pop() {
            match work {
                Work::Bindings(mut bs) => {
                    if let Some((v, e)) = bs.pop() {
                        let b = results.pop().unwrap();
                        todo.push(Work::Bindings(bs));
                        todo.push(Work::Expr(e, Context::Bind(v, b)));
                    }
                }
                Work::Lambda(params, k, rep) => {
                    let body = results.pop().unwrap();
                    results.push(self.rhs(
                        Rhs::Lambda(Lambda { params, body }),
                        k,
                        rep,
                    ));
                }
                Work::Rec(bs) => {
                    let bodies =
                        results.split_off(results.len() - bs.len());
                    let rest = results.pop().unwrap();
                    let bs = bs
                        .into_iter()
                        .zip(bodies)
                        .map(|((v, params), body)| {
                            (v, Lambda { params, body })
                        })
                        .collect();
                    results.push(Rc::new(Anf::LetRec(bs, rest)));
                }
                Work::Case(e, v, heads, cont) => {
                    let bodies =
                        results.split_off(results.len() - heads.len());
                    let alts = heads
                        .into_iter()
                        .zip(bodies)
                        .map(|((c, ps), b)| (c, ps, b))
                        .collect();
                    let b = wrap(
                        Rc::new(Anf::Case(Atom::Var(v.clone()), alts)),
                        cont,
                    );
                    todo.push(Work::Expr(e, Context::Bind(v, b)));
                }
                Work::Join(j, ps, cont) => {
                    let rest = results.pop().unwrap();
                    let body = results.pop().unwrap();
                    results.push(wrap(
                        Rc::new(Anf::Join(j, ps, body, rest)),
                        cont,
                    ));
                }
                Work::Call(head, f, args, result, rep) => {
                    let rest = results.pop().unwrap();
                    let vars: Vec<_> =
                        args.iter().map(|e| self.fresh(e.rep)).collect();
                    let atoms =
                        vars.iter().cloned().map(Atom::Var).collect();
                    let b = self.rhs(
                        Rhs::Call(Atom::Var(f.clone()), atoms),
                        Context::Bind(result, rest),
                        rep,
                    );
                    let bs = std::iter::once((f, head))
                        .chain(vars.into_iter().zip(args))
                        .collect();
                    results.push(b);
                    todo.push(Work::Bindings(bs));
                }
                Work::Expr(e, k) => {
                    let rep = e.rep;
                    match e.node {
                        E::Var(v) => {
                            results.push(self.finish(Atom::Var(v), k))
                        }
                        E::Global(v) => {
                            results.push(self.finish(Atom::Global(v), k))
                        }
                        E::Lit(l) => {
                            results.push(self.finish(Atom::Lit(l), k))
                        }
                        node @ E::Lam(..) => {
                            let (ps, body) =
                                peel(erase::Expr { node, rep });
                            todo.push(Work::Lambda(ps, k, rep));
                            todo.push(Work::Expr(body, Context::Return));
                        }
                        node @ E::App(..) => {
                            let mut head = erase::Expr { node, rep };
                            let mut args = vec![];
                            let mut reps = vec![];
                            while let E::App(f, a) = head.node {
                                reps.push(head.rep);
                                args.push(*a);
                                head = *f;
                            }
                            args.reverse();
                            reps.reverse();
                            let known = match &head.node {
                                E::Var(v) | E::Global(v) => {
                                    self.arities.get(v).copied()
                                }
                                E::Lam(..) => Some(arity(&head)),
                                _ => None,
                            };
                            // Unknown calls execute before evaluating their next argument.
                            let n = known.unwrap_or(1).min(args.len());
                            let extra = args.split_off(n);
                            let extra_reps = reps.split_off(n);
                            let f = self.fresh(head.rep);
                            if extra.is_empty() {
                                let vars: Vec<_> = args
                                    .iter()
                                    .map(|e| self.fresh(e.rep))
                                    .collect();
                                let atoms = vars
                                    .iter()
                                    .cloned()
                                    .map(Atom::Var)
                                    .collect();
                                results.push(self.rhs(
                                    Rhs::Call(Atom::Var(f.clone()), atoms),
                                    k,
                                    rep,
                                ));
                                todo.push(Work::Bindings(
                                    std::iter::once((f, head))
                                        .chain(vars.into_iter().zip(args))
                                        .collect(),
                                ));
                            } else {
                                let result = self.fresh(Rep::Word);
                                let application =
                                    extra.into_iter().zip(extra_reps).fold(
                                        erase::Expr {
                                            node: E::Var(result.clone()),
                                            rep: Rep::Word,
                                        },
                                        |f, (a, rep)| erase::Expr {
                                            node: E::App(
                                                Box::new(f),
                                                Box::new(a),
                                            ),
                                            rep,
                                        },
                                    );
                                todo.push(Work::Call(
                                    head,
                                    f,
                                    args,
                                    result,
                                    Rep::Word,
                                ));
                                todo.push(Work::Expr(application, k));
                            }
                        }
                        E::Let(rec, bs, b) => {
                            if rec {
                                let (heads, bodies): (Vec<_>, Vec<_>) = bs
                                    .into_iter()
                                    .map(|(v, e)| {
                                        let (ps, e) = peel(e);
                                        ((v, ps), e)
                                    })
                                    .unzip();
                                todo.push(Work::Rec(heads));
                                todo.extend(bodies.into_iter().rev().map(
                                    |e| Work::Expr(e, Context::Return),
                                ));
                            } else {
                                todo.push(Work::Bindings(bs));
                            }
                            todo.push(Work::Expr(*b, k));
                        }
                        node @ (E::Con(..) | E::Prim(..) | E::Jump(..)) => {
                            let args = match &node {
                                E::Con(_, es)
                                | E::Prim(_, es, _)
                                | E::Jump(_, es) => es,
                                _ => unreachable!(),
                            };
                            let vars: Vec<_> = args
                                .iter()
                                .map(|e| self.fresh(e.rep))
                                .collect();
                            let atoms = vars
                                .iter()
                                .cloned()
                                .map(Atom::Var)
                                .collect();
                            let (args, body) = match node {
                                E::Con(c, es) => (
                                    es,
                                    self.rhs(Rhs::Con(c, atoms), k, rep),
                                ),
                                E::Prim(p, es, s) => (
                                    es,
                                    self.rhs(
                                        Rhs::Prim(p, atoms, s),
                                        k,
                                        rep,
                                    ),
                                ),
                                E::Jump(j, es) => {
                                    (es, Rc::new(Anf::Jump(j, atoms)))
                                }
                                _ => unreachable!(),
                            };
                            results.push(body);
                            todo.push(Work::Bindings(
                                vars.into_iter().zip(args).collect(),
                            ));
                        }
                        E::Case(e, v, alts) => {
                            let (k, cont) = branch(k, self.sess);
                            let (heads, bodies): (Vec<_>, Vec<_>) = alts
                                .into_iter()
                                .map(|(c, ps, b)| ((c, ps), b))
                                .unzip();
                            todo.push(Work::Case(*e, v, heads, cont));
                            todo.extend(
                                bodies
                                    .into_iter()
                                    .rev()
                                    .map(|e| Work::Expr(e, k.clone())),
                            );
                        }
                        E::Join(j, ps, b, r) => {
                            let (k, cont) = branch(k, self.sess);
                            todo.push(Work::Join(j, ps, cont));
                            todo.push(Work::Expr(*r, k.clone()));
                            todo.push(Work::Expr(*b, k));
                        }
                        E::Fail(s) => results.push(Rc::new(Anf::Fail(s))),
                    }
                }
            }
        }
        assert_eq!(results.len(), 1);
        results.pop().unwrap()
    }
}
/// Normalize left-to-right, sharing every non-tail continuation exactly once.
pub fn convert(sess: &mut Session, program: erase::Program) -> Program {
    let mut arities = BTreeMap::new();
    for (v, e) in &program.globals {
        if arity(e) > 0 {
            arities.insert(v.clone(), arity(e));
        }
        collect(e, &mut arities);
    }
    let mut c = Convert {
        sess,
        arities,
        reps: program.reps,
    };
    let globals = program
        .globals
        .into_iter()
        .map(|(v, e)| (v, c.expr(e, Context::Return)))
        .collect();
    Program {
        reps: c.reps,
        globals,
        metadata: program.metadata,
    }
}
impl std::fmt::Display for Atom {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(v) => write!(f, "{v}"),
            Self::Global(v) => write!(f, "@{v}"),
            Self::Lit(l) => write!(f, "{l}"),
        }
    }
}
fn list<T: std::fmt::Display>(items: &[T]) -> String {
    items
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::Call(a, args) => write!(f, "call {a}({})", list(args)),
            Self::Con(c, args) => write!(f, "{}({})", c.name, list(args)),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::Lambda(l) => {
                write!(f, "lambda ({}) {{ {} }}", list(&l.params), l.body)
            }
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
        }
    }
}
impl std::fmt::Display for Anf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Body(&'a Anf),
            Rhs(&'a Rhs),
            Text(String),
        }
        let mut todo = vec![Work::Body(self)];
        while let Some(work) = todo.pop() {
            let mut parts = vec![];
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Rhs(Rhs::Lambda(l)) => {
                    write!(f, "lambda ({}) {{ ", list(&l.params))?;
                    parts.extend([
                        Work::Body(&l.body),
                        Work::Text(" }".into()),
                    ]);
                }
                Work::Rhs(r) => write!(f, "{r}")?,
                Work::Body(b) => match b {
                    Self::Let(v, r, b) => {
                        write!(f, "let {v} = ")?;
                        parts.extend([
                            Work::Rhs(r),
                            Work::Text(";\n".into()),
                            Work::Body(b),
                        ]);
                    }
                    Self::LetRec(bs, b) => {
                        for (v, l) in bs {
                            parts.extend([
                                Work::Text(format!(
                                    "rec {v}({}) = {{ ",
                                    list(&l.params)
                                )),
                                Work::Body(&l.body),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Body(b));
                    }
                    Self::Join(j, ps, b, r) => {
                        write!(f, "join {j}({}) = {{ ", list(ps))?;
                        parts.extend([
                            Work::Body(b),
                            Work::Text(" };\n".into()),
                            Work::Body(r),
                        ]);
                    }
                    Self::Jump(j, args) => {
                        write!(f, "jump {j}({})", list(args))?
                    }
                    Self::Case(a, alts) => {
                        writeln!(f, "case {a} {{")?;
                        for (c, ps, b) in alts {
                            let head = match c {
                                AltCon::Con(c) => c.name.clone(),
                                AltCon::Lit(l) => l.to_string(),
                                AltCon::Default => "_".into(),
                            };
                            parts.extend([
                                Work::Text(format!(
                                    "{head}({}) -> {{ ",
                                    list(ps)
                                )),
                                Work::Body(b),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Text("}".into()));
                    }
                    Self::Ret(a) => write!(f, "return {a}")?,
                    Self::TailCall(fun, args) => {
                        write!(f, "tail {fun}({})", list(args))?
                    }
                    Self::Fail(s) => write!(f, "fail {s:?}")?,
                },
            }
            todo.extend(parts.into_iter().rev());
        }
        Ok(())
    }
}
/// Drain child edges explicitly when the last owner releases a deep ANF tree.
impl Drop for Anf {
    fn drop(&mut self) {
        fn edges(node: &mut Anf, todo: &mut Vec<Body>) {
            fn take(b: &mut Body) -> Body {
                std::mem::replace(
                    b,
                    Rc::new(Anf::Ret(Atom::Lit(Lit::Int(0)))),
                )
            }
            match node {
                Anf::Let(_, r, b) => {
                    todo.push(take(b));
                    if let Rhs::Lambda(l) = r {
                        todo.push(take(&mut l.body));
                    }
                }
                Anf::LetRec(bs, b) => {
                    todo.push(take(b));
                    todo.extend(bs.drain(..).map(|(_, l)| l.body));
                }
                Anf::Join(_, _, b, r) => {
                    todo.push(take(b));
                    todo.push(take(r));
                }
                Anf::Case(_, alts) => {
                    todo.extend(alts.drain(..).map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        edges(self, &mut todo);
        while let Some(b) = todo.pop() {
            if let Ok(mut node) = Rc::try_unwrap(b) {
                edges(&mut node, &mut todo);
                // Empty this node before Drop runs again on the local variable.
                // Its remaining children are only leaf placeholders.
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Lambda parameters are merged only across syntactically adjacent lambdas.
pub struct Lambda {
    pub params: Vec<Var>,
    pub body: Body,
}
use crate::{
    ast::Op,
    core::{self, AltCon, Lit},
    data::ConId,
    erase,
    names::Id,
    session::Session,
    source::Span,
};
use std::{collections::BTreeMap, rc::Rc};
/// An erased binder keeps its unique identity and diagnostic spelling.
pub type Var = Id;
/// Syntax is immutable and shared between closures and machine states.
pub type Body = Rc<Anf>;
#[derive(Clone, Debug, PartialEq, Eq)]
/// Atoms cannot perform work or fail when read from a well-scoped environment.
pub enum Atom {
    Var(Var),
    Global(Id),
    Lit(Lit),
}
#[derive(Clone, Debug)]
/// Lambda parameters are merged only across syntactically adjacent lambdas.
pub struct Lambda {
    pub params: Vec<Var>,
    pub body: Body,
}
#[derive(Clone, Debug)]
/// Jumps transfer within a function; tail calls replace its activation.
pub enum Anf {
    Let(Var, Rhs, Body),
    LetRec(Vec<(Var, Lambda)>, Body),
    Join(Id, Vec<Var>, Body, Body),
    Jump(Id, Vec<Atom>),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Ret(Atom),
    TailCall(Atom, Vec<Atom>),
    Fail(Span),
}
#[derive(Clone, Debug)]
/// Global initialization order is preserved from typed Core.
pub struct Program {
    pub globals: Vec<(Id, Body)>,
    pub metadata: core::Program,
    pub reps: BTreeMap<Var, crate::rep::Rep>,
}
#[derive(Clone)]
enum Context {
    Return,
    Jump(Id),
    Bind(Var, Body),
}
struct Convert<'a> {
    sess: &'a mut Session,
    arities: BTreeMap<Id, usize>,
    reps: BTreeMap<Var, crate::rep::Rep>,
}
fn arity(mut e: &erase::Expr) -> usize {
    let mut n = 0;
    while let erase::Node::Lam(_, b) = &e.node {
        n += 1;
        e = b;
    }
    n
}
fn collect(e: &erase::Expr, out: &mut BTreeMap<Id, usize>) {
    use erase::Node as E;
    let mut todo = vec![e];
    while let Some(e) = todo.pop() {
        match &e.node {
            E::Let(_, bs, b) => {
                for (v, e) in bs {
                    let n = arity(e);
                    if n > 0 {
                        out.insert(v.clone(), n);
                    }
                    todo.push(e);
                }
                todo.push(b);
            }
            E::Lam(_, b) => todo.push(b),
            E::App(a, b) | E::Join(_, _, a, b) => {
                todo.push(a);
                todo.push(b);
            }
            E::Con(_, es) | E::Prim(_, es, _) | E::Jump(_, es) => {
                todo.extend(es)
            }
            E::Case(e, _, alts) => {
                todo.push(e);
                todo.extend(alts.iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
impl Convert<'_> {
    fn fresh(&mut self, rep: crate::rep::Rep) -> Var {
        let id = self.sess.fresh_id("$a");
        self.reps.insert(id.clone(), rep);
        id
    }
    fn finish(&mut self, a: Atom, k: Context) -> Body {
        Rc::new(match k {
            Context::Return => Anf::Ret(a),
            Context::Jump(j) => Anf::Jump(j, vec![a]),
            Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
        })
    }
    fn rhs(&mut self, r: Rhs, k: Context, rep: crate::rep::Rep) -> Body {
        if let Context::Bind(v, b) = k {
            return Rc::new(Anf::Let(v, r, b));
        }
        if let (Rhs::Call(f, args), Context::Return) = (&r, &k) {
            return Rc::new(Anf::TailCall(f.clone(), args.clone()));
        }
        let v = self.fresh(rep);
        let rest = self.finish(Atom::Var(v.clone()), k);
        Rc::new(Anf::Let(v, r, rest))
    }
    /// Tasks hold pending compiler work. Neither expression depth nor the
    /// number of operands adds a Rust call frame.
    fn expr(&mut self, e: erase::Expr, k: Context) -> Body {
        use crate::rep::Rep;
        use erase::Node as E;
        enum Work {
            Expr(erase::Expr, Context),
            Bindings(Vec<(Var, erase::Expr)>),
            Lambda(Vec<Var>, Context, Rep),
            Rec(Vec<(Var, Vec<Var>)>),
            Case(
                erase::Expr,
                Var,
                Vec<(AltCon, Vec<Var>)>,
                Option<(Id, Var, Body)>,
            ),
            Join(Id, Vec<Var>, Option<(Id, Var, Body)>),
            Call(erase::Expr, Var, Vec<erase::Expr>, Var, Rep),
        }
        fn peel(mut e: erase::Expr) -> (Vec<Var>, erase::Expr) {
            let mut ps = vec![];
            while let E::Lam(v, b) = e.node {
                ps.push(v);
                e = *b;
            }
            (ps, e)
        }
        fn branch(
            k: Context,
            sess: &mut Session,
        ) -> (Context, Option<(Id, Var, Body)>) {
            match k {
                Context::Bind(v, b) => {
                    let j = sess.fresh_id("$cont");
                    (Context::Jump(j.clone()), Some((j, v, b)))
                }
                k => (k, None),
            }
        }
        fn wrap(body: Body, cont: Option<(Id, Var, Body)>) -> Body {
            match cont {
                Some((j, v, b)) => Rc::new(Anf::Join(j, vec![v], b, body)),
                None => body,
            }
        }
        let mut todo = vec![Work::Expr(e, k)];
        let mut results: Vec<Body> = vec![];
        while let Some(work) = todo.pop() {
            match work {
                Work::Bindings(mut bs) => {
                    if let Some((v, e)) = bs.pop() {
                        let b = results.pop().unwrap();
                        todo.push(Work::Bindings(bs));
                        todo.push(Work::Expr(e, Context::Bind(v, b)));
                    }
                }
                Work::Lambda(params, k, rep) => {
                    let body = results.pop().unwrap();
                    results.push(self.rhs(
                        Rhs::Lambda(Lambda { params, body }),
                        k,
                        rep,
                    ));
                }
                Work::Rec(bs) => {
                    let bodies =
                        results.split_off(results.len() - bs.len());
                    let rest = results.pop().unwrap();
                    let bs = bs
                        .into_iter()
                        .zip(bodies)
                        .map(|((v, params), body)| {
                            (v, Lambda { params, body })
                        })
                        .collect();
                    results.push(Rc::new(Anf::LetRec(bs, rest)));
                }
                Work::Case(e, v, heads, cont) => {
                    let bodies =
                        results.split_off(results.len() - heads.len());
                    let alts = heads
                        .into_iter()
                        .zip(bodies)
                        .map(|((c, ps), b)| (c, ps, b))
                        .collect();
                    let b = wrap(
                        Rc::new(Anf::Case(Atom::Var(v.clone()), alts)),
                        cont,
                    );
                    todo.push(Work::Expr(e, Context::Bind(v, b)));
                }
                Work::Join(j, ps, cont) => {
                    let rest = results.pop().unwrap();
                    let body = results.pop().unwrap();
                    results.push(wrap(
                        Rc::new(Anf::Join(j, ps, body, rest)),
                        cont,
                    ));
                }
                Work::Call(head, f, args, result, rep) => {
                    let rest = results.pop().unwrap();
                    let vars: Vec<_> =
                        args.iter().map(|e| self.fresh(e.rep)).collect();
                    let atoms =
                        vars.iter().cloned().map(Atom::Var).collect();
                    let b = self.rhs(
                        Rhs::Call(Atom::Var(f.clone()), atoms),
                        Context::Bind(result, rest),
                        rep,
                    );
                    let bs = std::iter::once((f, head))
                        .chain(vars.into_iter().zip(args))
                        .collect();
                    results.push(b);
                    todo.push(Work::Bindings(bs));
                }
                Work::Expr(e, k) => {
                    let rep = e.rep;
                    match e.node {
                        E::Var(v) => {
                            results.push(self.finish(Atom::Var(v), k))
                        }
                        E::Global(v) => {
                            results.push(self.finish(Atom::Global(v), k))
                        }
                        E::Lit(l) => {
                            results.push(self.finish(Atom::Lit(l), k))
                        }
                        node @ E::Lam(..) => {
                            let (ps, body) =
                                peel(erase::Expr { node, rep });
                            todo.push(Work::Lambda(ps, k, rep));
                            todo.push(Work::Expr(body, Context::Return));
                        }
                        node @ E::App(..) => {
                            let mut head = erase::Expr { node, rep };
                            let mut args = vec![];
                            let mut reps = vec![];
                            while let E::App(f, a) = head.node {
                                reps.push(head.rep);
                                args.push(*a);
                                head = *f;
                            }
                            args.reverse();
                            reps.reverse();
                            let known = match &head.node {
                                E::Var(v) | E::Global(v) => {
                                    self.arities.get(v).copied()
                                }
                                E::Lam(..) => Some(arity(&head)),
                                _ => None,
                            };
                            // Unknown calls execute before evaluating their next argument.
                            let n = known.unwrap_or(1).min(args.len());
                            let extra = args.split_off(n);
                            let extra_reps = reps.split_off(n);
                            let f = self.fresh(head.rep);
                            if extra.is_empty() {
                                let vars: Vec<_> = args
                                    .iter()
                                    .map(|e| self.fresh(e.rep))
                                    .collect();
                                let atoms = vars
                                    .iter()
                                    .cloned()
                                    .map(Atom::Var)
                                    .collect();
                                results.push(self.rhs(
                                    Rhs::Call(Atom::Var(f.clone()), atoms),
                                    k,
                                    rep,
                                ));
                                todo.push(Work::Bindings(
                                    std::iter::once((f, head))
                                        .chain(vars.into_iter().zip(args))
                                        .collect(),
                                ));
                            } else {
                                let result = self.fresh(Rep::Word);
                                let application =
                                    extra.into_iter().zip(extra_reps).fold(
                                        erase::Expr {
                                            node: E::Var(result.clone()),
                                            rep: Rep::Word,
                                        },
                                        |f, (a, rep)| erase::Expr {
                                            node: E::App(
                                                Box::new(f),
                                                Box::new(a),
                                            ),
                                            rep,
                                        },
                                    );
                                todo.push(Work::Call(
                                    head,
                                    f,
                                    args,
                                    result,
                                    Rep::Word,
                                ));
                                todo.push(Work::Expr(application, k));
                            }
                        }
                        E::Let(rec, bs, b) => {
                            if rec {
                                let (heads, bodies): (Vec<_>, Vec<_>) = bs
                                    .into_iter()
                                    .map(|(v, e)| {
                                        let (ps, e) = peel(e);
                                        ((v, ps), e)
                                    })
                                    .unzip();
                                todo.push(Work::Rec(heads));
                                todo.extend(bodies.into_iter().rev().map(
                                    |e| Work::Expr(e, Context::Return),
                                ));
                            } else {
                                todo.push(Work::Bindings(bs));
                            }
                            todo.push(Work::Expr(*b, k));
                        }
                        node @ (E::Con(..) | E::Prim(..) | E::Jump(..)) => {
                            let args = match &node {
                                E::Con(_, es)
                                | E::Prim(_, es, _)
                                | E::Jump(_, es) => es,
                                _ => unreachable!(),
                            };
                            let vars: Vec<_> = args
                                .iter()
                                .map(|e| self.fresh(e.rep))
                                .collect();
                            let atoms = vars
                                .iter()
                                .cloned()
                                .map(Atom::Var)
                                .collect();
                            let (args, body) = match node {
                                E::Con(c, es) => (
                                    es,
                                    self.rhs(Rhs::Con(c, atoms), k, rep),
                                ),
                                E::Prim(p, es, s) => (
                                    es,
                                    self.rhs(
                                        Rhs::Prim(p, atoms, s),
                                        k,
                                        rep,
                                    ),
                                ),
                                E::Jump(j, es) => {
                                    (es, Rc::new(Anf::Jump(j, atoms)))
                                }
                                _ => unreachable!(),
                            };
                            results.push(body);
                            todo.push(Work::Bindings(
                                vars.into_iter().zip(args).collect(),
                            ));
                        }
                        E::Case(e, v, alts) => {
                            let (k, cont) = branch(k, self.sess);
                            let (heads, bodies): (Vec<_>, Vec<_>) = alts
                                .into_iter()
                                .map(|(c, ps, b)| ((c, ps), b))
                                .unzip();
                            todo.push(Work::Case(*e, v, heads, cont));
                            todo.extend(
                                bodies
                                    .into_iter()
                                    .rev()
                                    .map(|e| Work::Expr(e, k.clone())),
                            );
                        }
                        E::Join(j, ps, b, r) => {
                            let (k, cont) = branch(k, self.sess);
                            todo.push(Work::Join(j, ps, cont));
                            todo.push(Work::Expr(*r, k.clone()));
                            todo.push(Work::Expr(*b, k));
                        }
                        E::Fail(s) => results.push(Rc::new(Anf::Fail(s))),
                    }
                }
            }
        }
        assert_eq!(results.len(), 1);
        results.pop().unwrap()
    }
}
/// Normalize left-to-right, sharing every non-tail continuation exactly once.
pub fn convert(sess: &mut Session, program: erase::Program) -> Program {
    let mut arities = BTreeMap::new();
    for (v, e) in &program.globals {
        if arity(e) > 0 {
            arities.insert(v.clone(), arity(e));
        }
        collect(e, &mut arities);
    }
    let mut c = Convert {
        sess,
        arities,
        reps: program.reps,
    };
    let globals = program
        .globals
        .into_iter()
        .map(|(v, e)| (v, c.expr(e, Context::Return)))
        .collect();
    Program {
        reps: c.reps,
        globals,
        metadata: program.metadata,
    }
}
impl std::fmt::Display for Atom {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(v) => write!(f, "{v}"),
            Self::Global(v) => write!(f, "@{v}"),
            Self::Lit(l) => write!(f, "{l}"),
        }
    }
}
fn list<T: std::fmt::Display>(items: &[T]) -> String {
    items
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::Call(a, args) => write!(f, "call {a}({})", list(args)),
            Self::Con(c, args) => write!(f, "{}({})", c.name, list(args)),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::Lambda(l) => {
                write!(f, "lambda ({}) {{ {} }}", list(&l.params), l.body)
            }
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
        }
    }
}
impl std::fmt::Display for Anf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Body(&'a Anf),
            Rhs(&'a Rhs),
            Text(String),
        }
        let mut todo = vec![Work::Body(self)];
        while let Some(work) = todo.pop() {
            let mut parts = vec![];
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Rhs(Rhs::Lambda(l)) => {
                    write!(f, "lambda ({}) {{ ", list(&l.params))?;
                    parts.extend([
                        Work::Body(&l.body),
                        Work::Text(" }".into()),
                    ]);
                }
                Work::Rhs(r) => write!(f, "{r}")?,
                Work::Body(b) => match b {
                    Self::Let(v, r, b) => {
                        write!(f, "let {v} = ")?;
                        parts.extend([
                            Work::Rhs(r),
                            Work::Text(";\n".into()),
                            Work::Body(b),
                        ]);
                    }
                    Self::LetRec(bs, b) => {
                        for (v, l) in bs {
                            parts.extend([
                                Work::Text(format!(
                                    "rec {v}({}) = {{ ",
                                    list(&l.params)
                                )),
                                Work::Body(&l.body),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Body(b));
                    }
                    Self::Join(j, ps, b, r) => {
                        write!(f, "join {j}({}) = {{ ", list(ps))?;
                        parts.extend([
                            Work::Body(b),
                            Work::Text(" };\n".into()),
                            Work::Body(r),
                        ]);
                    }
                    Self::Jump(j, args) => {
                        write!(f, "jump {j}({})", list(args))?
                    }
                    Self::Case(a, alts) => {
                        writeln!(f, "case {a} {{")?;
                        for (c, ps, b) in alts {
                            let head = match c {
                                AltCon::Con(c) => c.name.clone(),
                                AltCon::Lit(l) => l.to_string(),
                                AltCon::Default => "_".into(),
                            };
                            parts.extend([
                                Work::Text(format!(
                                    "{head}({}) -> {{ ",
                                    list(ps)
                                )),
                                Work::Body(b),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Text("}".into()));
                    }
                    Self::Ret(a) => write!(f, "return {a}")?,
                    Self::TailCall(fun, args) => {
                        write!(f, "tail {fun}({})", list(args))?
                    }
                    Self::Fail(s) => write!(f, "fail {s:?}")?,
                },
            }
            todo.extend(parts.into_iter().rev());
        }
        Ok(())
    }
}
/// Drain child edges explicitly when the last owner releases a deep ANF tree.
impl Drop for Anf {
    fn drop(&mut self) {
        fn edges(node: &mut Anf, todo: &mut Vec<Body>) {
            fn take(b: &mut Body) -> Body {
                std::mem::replace(
                    b,
                    Rc::new(Anf::Ret(Atom::Lit(Lit::Int(0)))),
                )
            }
            match node {
                Anf::Let(_, r, b) => {
                    todo.push(take(b));
                    if let Rhs::Lambda(l) = r {
                        todo.push(take(&mut l.body));
                    }
                }
                Anf::LetRec(bs, b) => {
                    todo.push(take(b));
                    todo.extend(bs.drain(..).map(|(_, l)| l.body));
                }
                Anf::Join(_, _, b, r) => {
                    todo.push(take(b));
                    todo.push(take(r));
                }
                Anf::Case(_, alts) => {
                    todo.extend(alts.drain(..).map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        edges(self, &mut todo);
        while let Some(b) = todo.pop() {
            if let Ok(mut node) = Rc::try_unwrap(b) {
                edges(&mut node, &mut todo);
                // Empty this node before Drop runs again on the local variable.
                // Its remaining children are only leaf placeholders.
            }
        }
    }
}
#[derive(Clone, Debug)]
/// One strict computation whose operands have already been evaluated.
pub enum Rhs {
    Atom(Atom),
    Call(Atom, Vec<Atom>),
    Con(ConId, Vec<Atom>),
    Prim(Op, Vec<Atom>, Span),
    Lambda(Lambda),
    Field(Atom, u32),
}
use crate::{
    ast::Op,
    core::{self, AltCon, Lit},
    data::ConId,
    erase,
    names::Id,
    session::Session,
    source::Span,
};
use std::{collections::BTreeMap, rc::Rc};
/// An erased binder keeps its unique identity and diagnostic spelling.
pub type Var = Id;
/// Syntax is immutable and shared between closures and machine states.
pub type Body = Rc<Anf>;
#[derive(Clone, Debug, PartialEq, Eq)]
/// Atoms cannot perform work or fail when read from a well-scoped environment.
pub enum Atom {
    Var(Var),
    Global(Id),
    Lit(Lit),
}
#[derive(Clone, Debug)]
/// Lambda parameters are merged only across syntactically adjacent lambdas.
pub struct Lambda {
    pub params: Vec<Var>,
    pub body: Body,
}
#[derive(Clone, Debug)]
/// One strict computation whose operands have already been evaluated.
pub enum Rhs {
    Atom(Atom),
    Call(Atom, Vec<Atom>),
    Con(ConId, Vec<Atom>),
    Prim(Op, Vec<Atom>, Span),
    Lambda(Lambda),
    Field(Atom, u32),
}
#[derive(Clone, Debug)]
/// Global initialization order is preserved from typed Core.
pub struct Program {
    pub globals: Vec<(Id, Body)>,
    pub metadata: core::Program,
    pub reps: BTreeMap<Var, crate::rep::Rep>,
}
#[derive(Clone)]
enum Context {
    Return,
    Jump(Id),
    Bind(Var, Body),
}
struct Convert<'a> {
    sess: &'a mut Session,
    arities: BTreeMap<Id, usize>,
    reps: BTreeMap<Var, crate::rep::Rep>,
}
fn arity(mut e: &erase::Expr) -> usize {
    let mut n = 0;
    while let erase::Node::Lam(_, b) = &e.node {
        n += 1;
        e = b;
    }
    n
}
fn collect(e: &erase::Expr, out: &mut BTreeMap<Id, usize>) {
    use erase::Node as E;
    let mut todo = vec![e];
    while let Some(e) = todo.pop() {
        match &e.node {
            E::Let(_, bs, b) => {
                for (v, e) in bs {
                    let n = arity(e);
                    if n > 0 {
                        out.insert(v.clone(), n);
                    }
                    todo.push(e);
                }
                todo.push(b);
            }
            E::Lam(_, b) => todo.push(b),
            E::App(a, b) | E::Join(_, _, a, b) => {
                todo.push(a);
                todo.push(b);
            }
            E::Con(_, es) | E::Prim(_, es, _) | E::Jump(_, es) => {
                todo.extend(es)
            }
            E::Case(e, _, alts) => {
                todo.push(e);
                todo.extend(alts.iter().map(|(_, _, e)| e));
            }
            _ => {}
        }
    }
}
impl Convert<'_> {
    fn fresh(&mut self, rep: crate::rep::Rep) -> Var {
        let id = self.sess.fresh_id("$a");
        self.reps.insert(id.clone(), rep);
        id
    }
    fn finish(&mut self, a: Atom, k: Context) -> Body {
        Rc::new(match k {
            Context::Return => Anf::Ret(a),
            Context::Jump(j) => Anf::Jump(j, vec![a]),
            Context::Bind(v, b) => Anf::Let(v, Rhs::Atom(a), b),
        })
    }
    fn rhs(&mut self, r: Rhs, k: Context, rep: crate::rep::Rep) -> Body {
        if let Context::Bind(v, b) = k {
            return Rc::new(Anf::Let(v, r, b));
        }
        if let (Rhs::Call(f, args), Context::Return) = (&r, &k) {
            return Rc::new(Anf::TailCall(f.clone(), args.clone()));
        }
        let v = self.fresh(rep);
        let rest = self.finish(Atom::Var(v.clone()), k);
        Rc::new(Anf::Let(v, r, rest))
    }
    /// Tasks hold pending compiler work. Neither expression depth nor the
    /// number of operands adds a Rust call frame.
    fn expr(&mut self, e: erase::Expr, k: Context) -> Body {
        use crate::rep::Rep;
        use erase::Node as E;
        enum Work {
            Expr(erase::Expr, Context),
            Bindings(Vec<(Var, erase::Expr)>),
            Lambda(Vec<Var>, Context, Rep),
            Rec(Vec<(Var, Vec<Var>)>),
            Case(
                erase::Expr,
                Var,
                Vec<(AltCon, Vec<Var>)>,
                Option<(Id, Var, Body)>,
            ),
            Join(Id, Vec<Var>, Option<(Id, Var, Body)>),
            Call(erase::Expr, Var, Vec<erase::Expr>, Var, Rep),
        }
        fn peel(mut e: erase::Expr) -> (Vec<Var>, erase::Expr) {
            let mut ps = vec![];
            while let E::Lam(v, b) = e.node {
                ps.push(v);
                e = *b;
            }
            (ps, e)
        }
        fn branch(
            k: Context,
            sess: &mut Session,
        ) -> (Context, Option<(Id, Var, Body)>) {
            match k {
                Context::Bind(v, b) => {
                    let j = sess.fresh_id("$cont");
                    (Context::Jump(j.clone()), Some((j, v, b)))
                }
                k => (k, None),
            }
        }
        fn wrap(body: Body, cont: Option<(Id, Var, Body)>) -> Body {
            match cont {
                Some((j, v, b)) => Rc::new(Anf::Join(j, vec![v], b, body)),
                None => body,
            }
        }
        let mut todo = vec![Work::Expr(e, k)];
        let mut results: Vec<Body> = vec![];
        while let Some(work) = todo.pop() {
            match work {
                Work::Bindings(mut bs) => {
                    if let Some((v, e)) = bs.pop() {
                        let b = results.pop().unwrap();
                        todo.push(Work::Bindings(bs));
                        todo.push(Work::Expr(e, Context::Bind(v, b)));
                    }
                }
                Work::Lambda(params, k, rep) => {
                    let body = results.pop().unwrap();
                    results.push(self.rhs(
                        Rhs::Lambda(Lambda { params, body }),
                        k,
                        rep,
                    ));
                }
                Work::Rec(bs) => {
                    let bodies =
                        results.split_off(results.len() - bs.len());
                    let rest = results.pop().unwrap();
                    let bs = bs
                        .into_iter()
                        .zip(bodies)
                        .map(|((v, params), body)| {
                            (v, Lambda { params, body })
                        })
                        .collect();
                    results.push(Rc::new(Anf::LetRec(bs, rest)));
                }
                Work::Case(e, v, heads, cont) => {
                    let bodies =
                        results.split_off(results.len() - heads.len());
                    let alts = heads
                        .into_iter()
                        .zip(bodies)
                        .map(|((c, ps), b)| (c, ps, b))
                        .collect();
                    let b = wrap(
                        Rc::new(Anf::Case(Atom::Var(v.clone()), alts)),
                        cont,
                    );
                    todo.push(Work::Expr(e, Context::Bind(v, b)));
                }
                Work::Join(j, ps, cont) => {
                    let rest = results.pop().unwrap();
                    let body = results.pop().unwrap();
                    results.push(wrap(
                        Rc::new(Anf::Join(j, ps, body, rest)),
                        cont,
                    ));
                }
                Work::Call(head, f, args, result, rep) => {
                    let rest = results.pop().unwrap();
                    let vars: Vec<_> =
                        args.iter().map(|e| self.fresh(e.rep)).collect();
                    let atoms =
                        vars.iter().cloned().map(Atom::Var).collect();
                    let b = self.rhs(
                        Rhs::Call(Atom::Var(f.clone()), atoms),
                        Context::Bind(result, rest),
                        rep,
                    );
                    let bs = std::iter::once((f, head))
                        .chain(vars.into_iter().zip(args))
                        .collect();
                    results.push(b);
                    todo.push(Work::Bindings(bs));
                }
                Work::Expr(e, k) => {
                    let rep = e.rep;
                    match e.node {
                        E::Var(v) => {
                            results.push(self.finish(Atom::Var(v), k))
                        }
                        E::Global(v) => {
                            results.push(self.finish(Atom::Global(v), k))
                        }
                        E::Lit(l) => {
                            results.push(self.finish(Atom::Lit(l), k))
                        }
                        node @ E::Lam(..) => {
                            let (ps, body) =
                                peel(erase::Expr { node, rep });
                            todo.push(Work::Lambda(ps, k, rep));
                            todo.push(Work::Expr(body, Context::Return));
                        }
                        node @ E::App(..) => {
                            let mut head = erase::Expr { node, rep };
                            let mut args = vec![];
                            let mut reps = vec![];
                            while let E::App(f, a) = head.node {
                                reps.push(head.rep);
                                args.push(*a);
                                head = *f;
                            }
                            args.reverse();
                            reps.reverse();
                            let known = match &head.node {
                                E::Var(v) | E::Global(v) => {
                                    self.arities.get(v).copied()
                                }
                                E::Lam(..) => Some(arity(&head)),
                                _ => None,
                            };
                            // Unknown calls execute before evaluating their next argument.
                            let n = known.unwrap_or(1).min(args.len());
                            let extra = args.split_off(n);
                            let extra_reps = reps.split_off(n);
                            let f = self.fresh(head.rep);
                            if extra.is_empty() {
                                let vars: Vec<_> = args
                                    .iter()
                                    .map(|e| self.fresh(e.rep))
                                    .collect();
                                let atoms = vars
                                    .iter()
                                    .cloned()
                                    .map(Atom::Var)
                                    .collect();
                                results.push(self.rhs(
                                    Rhs::Call(Atom::Var(f.clone()), atoms),
                                    k,
                                    rep,
                                ));
                                todo.push(Work::Bindings(
                                    std::iter::once((f, head))
                                        .chain(vars.into_iter().zip(args))
                                        .collect(),
                                ));
                            } else {
                                let result = self.fresh(Rep::Word);
                                let application =
                                    extra.into_iter().zip(extra_reps).fold(
                                        erase::Expr {
                                            node: E::Var(result.clone()),
                                            rep: Rep::Word,
                                        },
                                        |f, (a, rep)| erase::Expr {
                                            node: E::App(
                                                Box::new(f),
                                                Box::new(a),
                                            ),
                                            rep,
                                        },
                                    );
                                todo.push(Work::Call(
                                    head,
                                    f,
                                    args,
                                    result,
                                    Rep::Word,
                                ));
                                todo.push(Work::Expr(application, k));
                            }
                        }
                        E::Let(rec, bs, b) => {
                            if rec {
                                let (heads, bodies): (Vec<_>, Vec<_>) = bs
                                    .into_iter()
                                    .map(|(v, e)| {
                                        let (ps, e) = peel(e);
                                        ((v, ps), e)
                                    })
                                    .unzip();
                                todo.push(Work::Rec(heads));
                                todo.extend(bodies.into_iter().rev().map(
                                    |e| Work::Expr(e, Context::Return),
                                ));
                            } else {
                                todo.push(Work::Bindings(bs));
                            }
                            todo.push(Work::Expr(*b, k));
                        }
                        node @ (E::Con(..) | E::Prim(..) | E::Jump(..)) => {
                            let args = match &node {
                                E::Con(_, es)
                                | E::Prim(_, es, _)
                                | E::Jump(_, es) => es,
                                _ => unreachable!(),
                            };
                            let vars: Vec<_> = args
                                .iter()
                                .map(|e| self.fresh(e.rep))
                                .collect();
                            let atoms = vars
                                .iter()
                                .cloned()
                                .map(Atom::Var)
                                .collect();
                            let (args, body) = match node {
                                E::Con(c, es) => (
                                    es,
                                    self.rhs(Rhs::Con(c, atoms), k, rep),
                                ),
                                E::Prim(p, es, s) => (
                                    es,
                                    self.rhs(
                                        Rhs::Prim(p, atoms, s),
                                        k,
                                        rep,
                                    ),
                                ),
                                E::Jump(j, es) => {
                                    (es, Rc::new(Anf::Jump(j, atoms)))
                                }
                                _ => unreachable!(),
                            };
                            results.push(body);
                            todo.push(Work::Bindings(
                                vars.into_iter().zip(args).collect(),
                            ));
                        }
                        E::Case(e, v, alts) => {
                            let (k, cont) = branch(k, self.sess);
                            let (heads, bodies): (Vec<_>, Vec<_>) = alts
                                .into_iter()
                                .map(|(c, ps, b)| ((c, ps), b))
                                .unzip();
                            todo.push(Work::Case(*e, v, heads, cont));
                            todo.extend(
                                bodies
                                    .into_iter()
                                    .rev()
                                    .map(|e| Work::Expr(e, k.clone())),
                            );
                        }
                        E::Join(j, ps, b, r) => {
                            let (k, cont) = branch(k, self.sess);
                            todo.push(Work::Join(j, ps, cont));
                            todo.push(Work::Expr(*r, k.clone()));
                            todo.push(Work::Expr(*b, k));
                        }
                        E::Fail(s) => results.push(Rc::new(Anf::Fail(s))),
                    }
                }
            }
        }
        assert_eq!(results.len(), 1);
        results.pop().unwrap()
    }
}
/// Normalize left-to-right, sharing every non-tail continuation exactly once.
pub fn convert(sess: &mut Session, program: erase::Program) -> Program {
    let mut arities = BTreeMap::new();
    for (v, e) in &program.globals {
        if arity(e) > 0 {
            arities.insert(v.clone(), arity(e));
        }
        collect(e, &mut arities);
    }
    let mut c = Convert {
        sess,
        arities,
        reps: program.reps,
    };
    let globals = program
        .globals
        .into_iter()
        .map(|(v, e)| (v, c.expr(e, Context::Return)))
        .collect();
    Program {
        reps: c.reps,
        globals,
        metadata: program.metadata,
    }
}
impl std::fmt::Display for Atom {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Var(v) => write!(f, "{v}"),
            Self::Global(v) => write!(f, "@{v}"),
            Self::Lit(l) => write!(f, "{l}"),
        }
    }
}
fn list<T: std::fmt::Display>(items: &[T]) -> String {
    items
        .iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::Call(a, args) => write!(f, "call {a}({})", list(args)),
            Self::Con(c, args) => write!(f, "{}({})", c.name, list(args)),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::Lambda(l) => {
                write!(f, "lambda ({}) {{ {} }}", list(&l.params), l.body)
            }
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
        }
    }
}
impl std::fmt::Display for Anf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Work<'a> {
            Body(&'a Anf),
            Rhs(&'a Rhs),
            Text(String),
        }
        let mut todo = vec![Work::Body(self)];
        while let Some(work) = todo.pop() {
            let mut parts = vec![];
            match work {
                Work::Text(s) => write!(f, "{s}")?,
                Work::Rhs(Rhs::Lambda(l)) => {
                    write!(f, "lambda ({}) {{ ", list(&l.params))?;
                    parts.extend([
                        Work::Body(&l.body),
                        Work::Text(" }".into()),
                    ]);
                }
                Work::Rhs(r) => write!(f, "{r}")?,
                Work::Body(b) => match b {
                    Self::Let(v, r, b) => {
                        write!(f, "let {v} = ")?;
                        parts.extend([
                            Work::Rhs(r),
                            Work::Text(";\n".into()),
                            Work::Body(b),
                        ]);
                    }
                    Self::LetRec(bs, b) => {
                        for (v, l) in bs {
                            parts.extend([
                                Work::Text(format!(
                                    "rec {v}({}) = {{ ",
                                    list(&l.params)
                                )),
                                Work::Body(&l.body),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Body(b));
                    }
                    Self::Join(j, ps, b, r) => {
                        write!(f, "join {j}({}) = {{ ", list(ps))?;
                        parts.extend([
                            Work::Body(b),
                            Work::Text(" };\n".into()),
                            Work::Body(r),
                        ]);
                    }
                    Self::Jump(j, args) => {
                        write!(f, "jump {j}({})", list(args))?
                    }
                    Self::Case(a, alts) => {
                        writeln!(f, "case {a} {{")?;
                        for (c, ps, b) in alts {
                            let head = match c {
                                AltCon::Con(c) => c.name.clone(),
                                AltCon::Lit(l) => l.to_string(),
                                AltCon::Default => "_".into(),
                            };
                            parts.extend([
                                Work::Text(format!(
                                    "{head}({}) -> {{ ",
                                    list(ps)
                                )),
                                Work::Body(b),
                                Work::Text(" }\n".into()),
                            ]);
                        }
                        parts.push(Work::Text("}".into()));
                    }
                    Self::Ret(a) => write!(f, "return {a}")?,
                    Self::TailCall(fun, args) => {
                        write!(f, "tail {fun}({})", list(args))?
                    }
                    Self::Fail(s) => write!(f, "fail {s:?}")?,
                },
            }
            todo.extend(parts.into_iter().rev());
        }
        Ok(())
    }
}
/// Drain child edges explicitly when the last owner releases a deep ANF tree.
impl Drop for Anf {
    fn drop(&mut self) {
        fn edges(node: &mut Anf, todo: &mut Vec<Body>) {
            fn take(b: &mut Body) -> Body {
                std::mem::replace(
                    b,
                    Rc::new(Anf::Ret(Atom::Lit(Lit::Int(0)))),
                )
            }
            match node {
                Anf::Let(_, r, b) => {
                    todo.push(take(b));
                    if let Rhs::Lambda(l) = r {
                        todo.push(take(&mut l.body));
                    }
                }
                Anf::LetRec(bs, b) => {
                    todo.push(take(b));
                    todo.extend(bs.drain(..).map(|(_, l)| l.body));
                }
                Anf::Join(_, _, b, r) => {
                    todo.push(take(b));
                    todo.push(take(r));
                }
                Anf::Case(_, alts) => {
                    todo.extend(alts.drain(..).map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
        let mut todo = vec![];
        edges(self, &mut todo);
        while let Some(b) = todo.pop() {
            if let Ok(mut node) = Rc::try_unwrap(b) {
                edges(&mut node, &mut todo);
                // Empty this node before Drop runs again on the local variable.
                // Its remaining children are only leaf placeholders.
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Jumps transfer within a function; tail calls replace its activation.
pub enum Anf {
    Let(Var, Rhs, Body),
    LetRec(Vec<(Var, Lambda)>, Body),
    Join(Id, Vec<Var>, Body, Body),
    Jump(Id, Vec<Atom>),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Ret(Atom),
    TailCall(Atom, Vec<Atom>),
    Fail(Span),
}
}

Ret은 현재 컨티뉴에이션으로 돌아갑니다. TailCall은 현재 활성화를 대체합니다. Jump는 그 활성화 안에 머뭅니다. 꼬리 위치가 아닌 소스 case는 자신의 대안들을 나머지 계산을 나타내는 하나의 join으로 보냅니다. 반복적 ANF 머신은 이 구분을 이용해 재귀적인 Rust 호출 없이 깊은 꼬리 재귀를 실행합니다.

Flat IR

모든 람다는 끌어올려진 함수가 되었습니다. 람다가 값을 캡처하면 그 환경이 명시적으로 드러납니다. 함수 인덱스는 프로그램의 함수 테이블을 가리킵니다.

#![allow(unused)]
fn main() {
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
    pub reps: crate::rep::Table,
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Visit<'a> {
            Body(&'a Flat),
            Text(String),
        }
        let mut work = vec![Visit::Body(self)];
        while let Some(next) = work.pop() {
            let Visit::Body(node) = next else {
                if let Visit::Text(text) = next {
                    f.write_str(&text)?;
                }
                continue;
            };
            match node {
                Self::Let(v, r, b) => {
                    writeln!(f, "let {v} = {r};")?;
                    work.push(Visit::Body(b));
                }
                Self::Join(j, ps, b, r) => {
                    write!(f, "join {j}({}) = {{ ", list(ps))?;
                    work.extend([
                        Visit::Body(r),
                        Visit::Text(" };\n".into()),
                        Visit::Body(b),
                    ]);
                }
                Self::Jump(j, args) => {
                    write!(f, "jump {j}({})", list(args))?
                }
                Self::Ret(a) => write!(f, "return {a}")?,
                Self::TailCallKnown(id, env, args) => write!(
                    f,
                    "tail {id}[{}]({})",
                    env.as_ref()
                        .map(ToString::to_string)
                        .unwrap_or_default(),
                    list(args)
                )?,
                Self::TailCallUnknown(a, args) => {
                    write!(f, "tail-apply {a}({})", list(args))?
                }
                Self::Fail(s) => write!(f, "fail {s:?}")?,
                Self::Case(a, alts) => {
                    writeln!(f, "case {a} {{")?;
                    work.push(Visit::Text("}".into()));
                    for (c, ps, b) in alts.iter().rev() {
                        let head = match c {
                            AltCon::Con(c) => c.name.clone(),
                            AltCon::Lit(l) => l.to_string(),
                            AltCon::Default => "_".into(),
                        };
                        work.extend([
                            Visit::Text(" }\n".into()),
                            Visit::Body(b),
                            Visit::Text(format!(
                                "{head}({}) -> {{ ",
                                list(ps)
                            )),
                        ]);
                    }
                }
            }
        }
        Ok(())
    }
}
/// Drain owned IR edges explicitly: even Rc destruction can otherwise recurse deeply.
impl Drop for Program {
    fn drop(&mut self) {
        let mut work = self
            .funcs
            .drain(..)
            .map(|f| f.body)
            .chain(self.globals.drain(..).map(|g| g.body))
            .collect::<Vec<_>>();
        while let Some(body) = work.pop() {
            let Ok(node) = Rc::try_unwrap(body) else {
                continue;
            };
            match node {
                Flat::Let(_, _, rest) => work.push(rest),
                Flat::Join(_, _, body, rest) => work.extend([body, rest]),
                Flat::Case(_, alts) => {
                    work.extend(alts.into_iter().map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
    pub reps: crate::rep::Table,
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Visit<'a> {
            Body(&'a Flat),
            Text(String),
        }
        let mut work = vec![Visit::Body(self)];
        while let Some(next) = work.pop() {
            let Visit::Body(node) = next else {
                if let Visit::Text(text) = next {
                    f.write_str(&text)?;
                }
                continue;
            };
            match node {
                Self::Let(v, r, b) => {
                    writeln!(f, "let {v} = {r};")?;
                    work.push(Visit::Body(b));
                }
                Self::Join(j, ps, b, r) => {
                    write!(f, "join {j}({}) = {{ ", list(ps))?;
                    work.extend([
                        Visit::Body(r),
                        Visit::Text(" };\n".into()),
                        Visit::Body(b),
                    ]);
                }
                Self::Jump(j, args) => {
                    write!(f, "jump {j}({})", list(args))?
                }
                Self::Ret(a) => write!(f, "return {a}")?,
                Self::TailCallKnown(id, env, args) => write!(
                    f,
                    "tail {id}[{}]({})",
                    env.as_ref()
                        .map(ToString::to_string)
                        .unwrap_or_default(),
                    list(args)
                )?,
                Self::TailCallUnknown(a, args) => {
                    write!(f, "tail-apply {a}({})", list(args))?
                }
                Self::Fail(s) => write!(f, "fail {s:?}")?,
                Self::Case(a, alts) => {
                    writeln!(f, "case {a} {{")?;
                    work.push(Visit::Text("}".into()));
                    for (c, ps, b) in alts.iter().rev() {
                        let head = match c {
                            AltCon::Con(c) => c.name.clone(),
                            AltCon::Lit(l) => l.to_string(),
                            AltCon::Default => "_".into(),
                        };
                        work.extend([
                            Visit::Text(" }\n".into()),
                            Visit::Body(b),
                            Visit::Text(format!(
                                "{head}({}) -> {{ ",
                                list(ps)
                            )),
                        ]);
                    }
                }
            }
        }
        Ok(())
    }
}
/// Drain owned IR edges explicitly: even Rc destruction can otherwise recurse deeply.
impl Drop for Program {
    fn drop(&mut self) {
        let mut work = self
            .funcs
            .drain(..)
            .map(|f| f.body)
            .chain(self.globals.drain(..).map(|g| g.body))
            .collect::<Vec<_>>();
        while let Some(body) = work.pop() {
            let Ok(node) = Rc::try_unwrap(body) else {
                continue;
            };
            match node {
                Flat::Let(_, _, rest) => work.push(rest),
                Flat::Join(_, _, body, rest) => work.extend([body, rest]),
                Flat::Case(_, alts) => {
                    work.extend(alts.into_iter().map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Visit<'a> {
            Body(&'a Flat),
            Text(String),
        }
        let mut work = vec![Visit::Body(self)];
        while let Some(next) = work.pop() {
            let Visit::Body(node) = next else {
                if let Visit::Text(text) = next {
                    f.write_str(&text)?;
                }
                continue;
            };
            match node {
                Self::Let(v, r, b) => {
                    writeln!(f, "let {v} = {r};")?;
                    work.push(Visit::Body(b));
                }
                Self::Join(j, ps, b, r) => {
                    write!(f, "join {j}({}) = {{ ", list(ps))?;
                    work.extend([
                        Visit::Body(r),
                        Visit::Text(" };\n".into()),
                        Visit::Body(b),
                    ]);
                }
                Self::Jump(j, args) => {
                    write!(f, "jump {j}({})", list(args))?
                }
                Self::Ret(a) => write!(f, "return {a}")?,
                Self::TailCallKnown(id, env, args) => write!(
                    f,
                    "tail {id}[{}]({})",
                    env.as_ref()
                        .map(ToString::to_string)
                        .unwrap_or_default(),
                    list(args)
                )?,
                Self::TailCallUnknown(a, args) => {
                    write!(f, "tail-apply {a}({})", list(args))?
                }
                Self::Fail(s) => write!(f, "fail {s:?}")?,
                Self::Case(a, alts) => {
                    writeln!(f, "case {a} {{")?;
                    work.push(Visit::Text("}".into()));
                    for (c, ps, b) in alts.iter().rev() {
                        let head = match c {
                            AltCon::Con(c) => c.name.clone(),
                            AltCon::Lit(l) => l.to_string(),
                            AltCon::Default => "_".into(),
                        };
                        work.extend([
                            Visit::Text(" }\n".into()),
                            Visit::Body(b),
                            Visit::Text(format!(
                                "{head}({}) -> {{ ",
                                list(ps)
                            )),
                        ]);
                    }
                }
            }
        }
        Ok(())
    }
}
/// Drain owned IR edges explicitly: even Rc destruction can otherwise recurse deeply.
impl Drop for Program {
    fn drop(&mut self) {
        let mut work = self
            .funcs
            .drain(..)
            .map(|f| f.body)
            .chain(self.globals.drain(..).map(|g| g.body))
            .collect::<Vec<_>>();
        while let Some(body) = work.pop() {
            let Ok(node) = Rc::try_unwrap(body) else {
                continue;
            };
            match node {
                Flat::Let(_, _, rest) => work.push(rest),
                Flat::Join(_, _, body, rest) => work.extend([body, rest]),
                Flat::Case(_, alts) => {
                    work.extend(alts.into_iter().map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
    }
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
    pub reps: crate::rep::Table,
}
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
    pub reps: crate::rep::Table,
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Visit<'a> {
            Body(&'a Flat),
            Text(String),
        }
        let mut work = vec![Visit::Body(self)];
        while let Some(next) = work.pop() {
            let Visit::Body(node) = next else {
                if let Visit::Text(text) = next {
                    f.write_str(&text)?;
                }
                continue;
            };
            match node {
                Self::Let(v, r, b) => {
                    writeln!(f, "let {v} = {r};")?;
                    work.push(Visit::Body(b));
                }
                Self::Join(j, ps, b, r) => {
                    write!(f, "join {j}({}) = {{ ", list(ps))?;
                    work.extend([
                        Visit::Body(r),
                        Visit::Text(" };\n".into()),
                        Visit::Body(b),
                    ]);
                }
                Self::Jump(j, args) => {
                    write!(f, "jump {j}({})", list(args))?
                }
                Self::Ret(a) => write!(f, "return {a}")?,
                Self::TailCallKnown(id, env, args) => write!(
                    f,
                    "tail {id}[{}]({})",
                    env.as_ref()
                        .map(ToString::to_string)
                        .unwrap_or_default(),
                    list(args)
                )?,
                Self::TailCallUnknown(a, args) => {
                    write!(f, "tail-apply {a}({})", list(args))?
                }
                Self::Fail(s) => write!(f, "fail {s:?}")?,
                Self::Case(a, alts) => {
                    writeln!(f, "case {a} {{")?;
                    work.push(Visit::Text("}".into()));
                    for (c, ps, b) in alts.iter().rev() {
                        let head = match c {
                            AltCon::Con(c) => c.name.clone(),
                            AltCon::Lit(l) => l.to_string(),
                            AltCon::Default => "_".into(),
                        };
                        work.extend([
                            Visit::Text(" }\n".into()),
                            Visit::Body(b),
                            Visit::Text(format!(
                                "{head}({}) -> {{ ",
                                list(ps)
                            )),
                        ]);
                    }
                }
            }
        }
        Ok(())
    }
}
/// Drain owned IR edges explicitly: even Rc destruction can otherwise recurse deeply.
impl Drop for Program {
    fn drop(&mut self) {
        let mut work = self
            .funcs
            .drain(..)
            .map(|f| f.body)
            .chain(self.globals.drain(..).map(|g| g.body))
            .collect::<Vec<_>>();
        while let Some(body) = work.pop() {
            let Ok(node) = Rc::try_unwrap(body) else {
                continue;
            };
            match node {
                Flat::Let(_, _, rest) => work.push(rest),
                Flat::Join(_, _, body, rest) => work.extend([body, rest]),
                Flat::Case(_, alts) => {
                    work.extend(alts.into_iter().map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
    }
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
pub use crate::anf::{Atom, Var};
use crate::{
    ast::Op,
    core::{self, AltCon},
    data::ConId,
    source::Span,
};
use std::rc::Rc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
/// An index into this program's lifted function table.
pub struct FuncId(pub u32);
pub type Body = Rc<Flat>;
#[derive(Clone, Debug)]
/// Only lifted local lambdas receive an environment parameter.
pub struct Func {
    pub name: FuncId,
    pub source: Var,
    pub env: Option<Var>,
    pub params: Vec<Var>,
    pub body: Body,
    pub captures: Vec<Var>,
}
#[derive(Clone, Debug)]
/// Function globals are installed before ordered strict value initializers run.
pub struct GlobalInit {
    pub name: Var,
    pub function: Option<FuncId>,
    pub body: Body,
}
#[derive(Clone, Debug)]
pub struct Program {
    pub funcs: Vec<Func>,
    pub globals: Vec<GlobalInit>,
    pub metadata: core::Program,
    pub reps: crate::rep::Table,
}
#[derive(Clone, Debug)]
/// PatchClosure is a compiler-only operation used after all letrec allocations.
pub enum Rhs {
    Atom(Atom),
    CallKnown(FuncId, Option<Atom>, Vec<Atom>),
    CallUnknown(Atom, Vec<Atom>),
    AllocCon(ConId, Vec<Atom>),
    AllocClosure(FuncId, Vec<Atom>),
    EnvField(Var, u32),
    Field(Atom, u32),
    Prim(Op, Vec<Atom>, Span),
    PatchClosure(Atom, u32, Atom),
}
impl std::fmt::Display for FuncId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "f{}", self.0)
    }
}
fn list<T: std::fmt::Display>(xs: &[T]) -> String {
    xs.iter()
        .map(ToString::to_string)
        .collect::<Vec<_>>()
        .join(", ")
}
impl std::fmt::Display for Rhs {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Atom(a) => write!(f, "{a}"),
            Self::CallKnown(id, env, args) => write!(
                f,
                "call {id}[{}]({})",
                env.as_ref().map(ToString::to_string).unwrap_or_default(),
                list(args)
            ),
            Self::CallUnknown(a, args) => {
                write!(f, "apply {a}({})", list(args))
            }
            Self::AllocCon(c, args) => {
                write!(f, "{}({})", c.name, list(args))
            }
            Self::AllocClosure(id, args) => {
                write!(f, "closure {id}[{}]", list(args))
            }
            Self::EnvField(v, i) => write!(f, "env {v}[{i}]"),
            Self::Field(a, i) => write!(f, "field {a}[{i}]"),
            Self::Prim(p, args, _) => {
                write!(f, "{}({})", p.text(), list(args))
            }
            Self::PatchClosure(a, i, v) => {
                write!(f, "patch {a}[{i}] = {v}")
            }
        }
    }
}
impl std::fmt::Display for Flat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        enum Visit<'a> {
            Body(&'a Flat),
            Text(String),
        }
        let mut work = vec![Visit::Body(self)];
        while let Some(next) = work.pop() {
            let Visit::Body(node) = next else {
                if let Visit::Text(text) = next {
                    f.write_str(&text)?;
                }
                continue;
            };
            match node {
                Self::Let(v, r, b) => {
                    writeln!(f, "let {v} = {r};")?;
                    work.push(Visit::Body(b));
                }
                Self::Join(j, ps, b, r) => {
                    write!(f, "join {j}({}) = {{ ", list(ps))?;
                    work.extend([
                        Visit::Body(r),
                        Visit::Text(" };\n".into()),
                        Visit::Body(b),
                    ]);
                }
                Self::Jump(j, args) => {
                    write!(f, "jump {j}({})", list(args))?
                }
                Self::Ret(a) => write!(f, "return {a}")?,
                Self::TailCallKnown(id, env, args) => write!(
                    f,
                    "tail {id}[{}]({})",
                    env.as_ref()
                        .map(ToString::to_string)
                        .unwrap_or_default(),
                    list(args)
                )?,
                Self::TailCallUnknown(a, args) => {
                    write!(f, "tail-apply {a}({})", list(args))?
                }
                Self::Fail(s) => write!(f, "fail {s:?}")?,
                Self::Case(a, alts) => {
                    writeln!(f, "case {a} {{")?;
                    work.push(Visit::Text("}".into()));
                    for (c, ps, b) in alts.iter().rev() {
                        let head = match c {
                            AltCon::Con(c) => c.name.clone(),
                            AltCon::Lit(l) => l.to_string(),
                            AltCon::Default => "_".into(),
                        };
                        work.extend([
                            Visit::Text(" }\n".into()),
                            Visit::Body(b),
                            Visit::Text(format!(
                                "{head}({}) -> {{ ",
                                list(ps)
                            )),
                        ]);
                    }
                }
            }
        }
        Ok(())
    }
}
/// Drain owned IR edges explicitly: even Rc destruction can otherwise recurse deeply.
impl Drop for Program {
    fn drop(&mut self) {
        let mut work = self
            .funcs
            .drain(..)
            .map(|f| f.body)
            .chain(self.globals.drain(..).map(|g| g.body))
            .collect::<Vec<_>>();
        while let Some(body) = work.pop() {
            let Ok(node) = Rc::try_unwrap(body) else {
                continue;
            };
            match node {
                Flat::Let(_, _, rest) => work.push(rest),
                Flat::Join(_, _, body, rest) => work.extend([body, rest]),
                Flat::Case(_, alts) => {
                    work.extend(alts.into_iter().map(|(_, _, b)| b))
                }
                _ => {}
            }
        }
    }
}
#[derive(Clone, Debug)]
/// Control transfers are explicit and joins remain local to each function.
pub enum Flat {
    Let(Var, Rhs, Body),
    Case(Atom, Vec<(AltCon, Vec<Var>, Body)>),
    Join(Var, Vec<Var>, Body, Body),
    Jump(Var, Vec<Atom>),
    Ret(Atom),
    TailCallKnown(FuncId, Option<Atom>, Vec<Atom>),
    TailCallUnknown(Atom, Vec<Atom>),
    Fail(Span),
}
}

알려진 호출은 올바른 개수의 인자로 끌어올려진 함수를 지정합니다. 알려지지 않은 호출은 eval/apply를 사용하며, 이 방식은 인자 수가 정확한 호출, 부분 호출, 과포화 호출을 지원합니다. PatchClosure는 그룹의 모든 클로저를 할당한 뒤 재귀적 캡처를 초기화합니다. 이는 내부 연산이며, 소스 언어의 변경 원시 연산이 결코 아닙니다.

수치 증거와 저장소

23장은 검사된 Core 타입에서 유도한 작은 분류를 보존합니다:

#![allow(unused)]
fn main() {
impl std::fmt::Display for Rep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            Self::Int => "Int",
            Self::Double => "Double",
            Self::Word => "Word",
        })
    }
}
use crate::{
    anf::{Anf, Atom, Rhs, Var},
    core,
    names::Id,
};
use std::collections::BTreeMap;
pub type Table = BTreeMap<Id, Rep>;
/// Missing evidence is an internal error, never an invitation to infer a storage tag.
pub fn get(table: &Table, id: &Id) -> Result<Rep, String> {
    table
        .get(id)
        .copied()
        .ok_or_else(|| format!("missing representation evidence for {id}"))
}
pub fn atom(table: &Table, a: &Atom) -> Result<Rep, String> {
    match a {
        Atom::Var(v) | Atom::Global(v) => get(table, v),
        Atom::Lit(l) => Ok(crate::erase::literal_rep(l)),
    }
}
/// Numeric operands are checked against the primitive's declared signature.
pub fn primitive(
    table: &Table,
    op: crate::ast::Op,
    args: &[Atom],
    result: &Var,
    metadata: &core::Program,
) -> Result<(), String> {
    use crate::primitives::Prim;
    let generic = matches!(
        op,
        crate::ast::Op::Runtime(Prim::Trace | Prim::Error | Prim::Exit)
    );
    let types = if generic {
        vec![core::Type::Var(0)]
    } else {
        vec![]
    };
    let (mut ty, _) = crate::primitives::core_signature(
        op,
        &types,
        &metadata.data_types,
    )?;
    for arg in args {
        let core::Type::Arrow(a, b) = ty else {
            return Err("primitive arity evidence".into());
        };
        if !matches!(a.as_ref(), core::Type::Var(_))
            && atom(table, arg)? != crate::erase::representation(&a)
        {
            return Err(format!(
                "contradictory operand evidence for {}: {:?}",
                op.text(),
                arg
            ));
        }
        ty = *b;
    }
    let expected = if matches!(op, crate::ast::Op::Runtime(Prim::Trace)) {
        atom(table, &args[1])?
    } else if matches!(ty, core::Type::Var(_)) {
        get(table, result)?
    } else {
        crate::erase::representation(&ty)
    };
    if get(table, result)? != expected {
        return Err(format!("contradictory result evidence for {result}"));
    }
    Ok(())
}
/// Copy evidence to the beta continuation's fresh result. No type classification occurs here.
pub fn beta_results(program: &mut crate::anf::Program) {
    let mut todo = program
        .globals
        .iter()
        .map(|(_, b)| b.as_ref())
        .collect::<Vec<_>>();
    let mut joins = BTreeMap::new();
    let mut pending = vec![];
    while let Some(b) = todo.pop() {
        match b {
            Anf::Let(v, r, rest) => {
                if !program.reps.contains_key(v) {
                    let Anf::Jump(j, _) = rest.as_ref() else {
                        panic!("beta result continuation")
                    };
                    pending.push((v.clone(), j.clone()));
                }
                if let Rhs::Lambda(l) = r {
                    todo.push(&l.body);
                }
                todo.push(rest);
            }
            Anf::LetRec(bs, r) => {
                todo.push(r);
                for (_, l) in bs {
                    todo.push(&l.body);
                }
            }
            Anf::Join(j, ps, b, r) => {
                if ps.len() == 1 {
                    joins.insert(j.clone(), program.reps[&ps[0]]);
                }
                todo.push(b);
                todo.push(r);
            }
            Anf::Case(_, alts) => {
                for (_, _, b) in alts {
                    todo.push(b);
                }
            }
            _ => {}
        }
    }
    for (v, j) in pending {
        program.reps.insert(v, joins[&j]);
    }
}
pub fn lint_anf(program: &crate::anf::Program) -> Result<(), String> {
    let mut todo = vec![];
    for (v, b) in &program.globals {
        get(&program.reps, v)?;
        todo.push(b.as_ref());
    }
    while let Some(b) = todo.pop() {
        match b {
            Anf::Let(v, r, rest) => {
                get(&program.reps, v)?;
                match r {
                    Rhs::Prim(p, args, _) => primitive(
                        &program.reps,
                        *p,
                        args,
                        v,
                        &program.metadata,
                    )?,
                    Rhs::Lambda(l) => todo.push(&l.body),
                    _ => {}
                }
                todo.push(rest);
            }
            Anf::LetRec(bs, r) => {
                todo.push(r);
                for (_, l) in bs {
                    todo.push(&l.body);
                }
            }
            Anf::Join(_, _, b, r) => {
                todo.push(b);
                todo.push(r);
            }
            Anf::Case(_, alts) => {
                for (_, _, b) in alts {
                    todo.push(b);
                }
            }
            _ => {}
        }
    }
    Ok(())
}
pub fn lint_flat(program: &crate::flat::Program) -> Result<(), String> {
    use crate::flat::{Flat, Rhs};
    let mut todo = program
        .funcs
        .iter()
        .map(|f| f.body.as_ref())
        .collect::<Vec<_>>();
    for g in &program.globals {
        get(&program.reps, &g.name)?;
        todo.push(g.body.as_ref());
    }
    while let Some(b) = todo.pop() {
        match b {
            Flat::Let(v, r, rest) => {
                get(&program.reps, v)?;
                if let Rhs::Prim(p, args, _) = r {
                    primitive(
                        &program.reps,
                        *p,
                        args,
                        v,
                        &program.metadata,
                    )?;
                }
                todo.push(rest);
            }
            Flat::Join(_, _, b, r) => {
                todo.push(b);
                todo.push(r);
            }
            Flat::Case(_, alts) => {
                for (_, _, b) in alts {
                    todo.push(b);
                }
            }
            _ => {}
        }
    }
    Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Rep {
    Int,
    Double,
    Word,
}
}

이는 타입 증거이지 호출 규약이 아닙니다. 호출 결과와 매개변수는 그 증거가 Int나 Double을 가리키더라도 워드로 남습니다. LLVM의 지역 위치는 워드, 네이티브 숫자, 다시 읽어 들일 수 있는 루트를 구분합니다:

#![allow(unused)]
fn main() {
use super::*;
use crate::{
    core::Lit,
    flat::{Atom, FuncId},
    runtime_layout as layout,
};
use inkwell::{
    types::BasicMetadataTypeEnum,
    values::{BasicMetadataValueEnum, PointerValue},
};
impl<'ctx> Backend<'ctx, '_> {
    pub(super) fn n(&self, n: u64) -> IntValue<'ctx> {
        self.word.const_int(n, false)
    }
    pub(super) fn external(
        &self,
        name: &str,
        args: &[BasicMetadataTypeEnum<'ctx>],
        result: bool,
    ) -> FunctionValue<'ctx> {
        self.module.get_function(name).unwrap_or_else(|| {
            let ty = if result {
                self.word.fn_type(args, false)
            } else {
                self.context.void_type().fn_type(args, false)
            };
            self.module.add_function(name, ty, None)
        })
    }
    pub(super) fn call(
        &self,
        name: &str,
        args: &[BasicMetadataValueEnum<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        let types = args
            .iter()
            .map(|a| match a {
                BasicMetadataValueEnum::IntValue(v) => v.get_type().into(),
                BasicMetadataValueEnum::PointerValue(v) => {
                    v.get_type().into()
                }
                _ => unreachable!("runtime ABI operands"),
            })
            .collect::<Vec<_>>();
        let call = self.builder.build_call(
            self.external(name, &types, true),
            args,
            "call",
        )?;
        // A tailcc function must regain control to restore its callee-pop frame.
        // LLVM's tail-call pass may otherwise infer a tail transfer into the C ABI.
        call.set_tail_call_kind(
            inkwell::values::LLVMTailCallKind::LLVMTailCallKindNoTail,
        );
        Ok(call.try_as_basic_value().unwrap_basic().into_int_value())
    }
    pub(super) fn void_call(
        &self,
        name: &str,
        args: &[BasicMetadataValueEnum<'ctx>],
    ) -> Result<()> {
        let types = args
            .iter()
            .map(|a| match a {
                BasicMetadataValueEnum::IntValue(v) => v.get_type().into(),
                BasicMetadataValueEnum::PointerValue(v) => {
                    v.get_type().into()
                }
                _ => unreachable!("runtime ABI operands"),
            })
            .collect::<Vec<_>>();
        let call = self.builder.build_call(
            self.external(name, &types, false),
            args,
            "",
        )?;
        if self.copying {
            call.set_tail_call_kind(
                inkwell::values::LLVMTailCallKind::LLVMTailCallKindNoTail,
            );
        }
        Ok(())
    }
    pub(super) fn pointer(
        &self,
        value: IntValue<'ctx>,
    ) -> Result<PointerValue<'ctx>> {
        Ok(self.builder.build_int_to_ptr(
            value,
            self.context.ptr_type(AddressSpace::default()),
            "ptr",
        )?)
    }
    pub(super) fn slot(
        &self,
        pointer: PointerValue<'ctx>,
        index: u64,
    ) -> Result<PointerValue<'ctx>> {
        // SAFETY: GEP indexes i64 words using the checked ABI layout.
        Ok(unsafe {
            self.builder.build_gep(
                self.word,
                pointer,
                &[self.n(index)],
                "slot",
            )?
        })
    }
    pub(super) fn load(
        &self,
        value: IntValue<'ctx>,
        index: u64,
    ) -> Result<IntValue<'ctx>> {
        Ok(self
            .builder
            .build_load(
                self.word,
                self.slot(self.pointer(value)?, index)?,
                "load",
            )?
            .into_int_value())
    }
    pub(super) fn store(
        &self,
        value: IntValue<'ctx>,
        index: u64,
        field: IntValue<'ctx>,
    ) -> Result<()> {
        self.builder
            .build_store(self.slot(self.pointer(value)?, index)?, field)?;
        Ok(())
    }
    pub(super) fn allocation(
        &self,
        kind: u8,
        tag: u32,
        fields: &[IntValue<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        let protect = self.copying
            && !matches!(kind, layout::DOUBLE | layout::STRING)
            && fields.len() > if kind == layout::CLOSURE { 2 } else { 0 };
        let mut roots = None;
        if protect {
            let entry = self
                .builder
                .get_insert_block()
                .unwrap()
                .get_parent()
                .unwrap()
                .get_first_basic_block()
                .unwrap();
            let alloc = self.context.create_builder();
            alloc.position_before(&entry.get_first_instruction().unwrap());
            let p = alloc.build_alloca(
                self.word.array_type(fields.len().max(1) as u32),
                "allocation.roots",
            )?;
            for (i, f) in fields.iter().enumerate() {
                self.builder.build_store(self.slot(p, i as u64)?, *f)?;
            }
            // Metadata fields (code, arity) are excluded from the root range.
            let skip = if kind == layout::CLOSURE { 2 } else { 0 };
            self.void_call(
                "rt_roots_push",
                &[
                    self.slot(p, skip)?.into(),
                    self.n(fields.len() as u64 - skip).into(),
                ],
            )?;
            roots = Some(p);
        }
        let name = if matches!(kind, layout::DOUBLE | layout::STRING) {
            "rt_alloc_atomic"
        } else {
            "rt_alloc"
        };
        let ptr = self.context.ptr_type(AddressSpace::default());
        let f = self.module.get_function(name).unwrap_or_else(|| {
            self.module.add_function(
                name,
                ptr.fn_type(&[self.word.into()], false),
                None,
            )
        });
        let size = (fields.len() + 1) as u64
            * if name == "rt_alloc" { 1 } else { 8 };
        let p = self
            .builder
            .build_call(f, &[self.n(size).into()], "alloc")?
            .try_as_basic_value()
            .unwrap_basic()
            .into_pointer_value();
        let value = self.builder.build_ptr_to_int(p, self.word, "word")?;
        self.store(
            value,
            0,
            self.n(layout::header(kind, tag, fields.len() as u32)),
        )?;
        for (i, field) in fields.iter().enumerate() {
            let field = if let Some(p) = roots {
                self.builder
                    .build_load(
                        self.word,
                        self.slot(p, i as u64)?,
                        "relocated.field",
                    )?
                    .into_int_value()
            } else {
                *field
            };
            self.store(value, i as u64 + 1, field)?;
        }
        if protect {
            self.void_call("rt_roots_pop", &[])?;
        }
        Ok(value)
    }
    pub(super) fn closure(
        &self,
        f: FuncId,
        captures: &[IntValue<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        let code = self.slow[f.0 as usize]
            .as_global_value()
            .as_pointer_value()
            .const_to_int(self.word);
        let mut fields = vec![
            code,
            self.n(self.program.funcs[f.0 as usize].params.len() as u64),
        ];
        fields.extend_from_slice(captures);
        self.allocation(layout::CLOSURE, 0, &fields)
    }
    fn string(&mut self, s: &str) -> IntValue<'ctx> {
        if let Some(v) = self.strings.get(s) {
            return *v;
        }
        let mut bytes = s.as_bytes().to_vec();
        bytes.resize(bytes.len().div_ceil(8) * 8, 0);
        let data = self.context.const_string(&bytes, false);
        let object = self.context.const_struct(
            &[
                self.n(layout::header(
                    layout::STRING,
                    0,
                    1 + (bytes.len() / 8) as u32,
                ))
                .into(),
                self.n(s.len() as u64).into(),
                data.into(),
            ],
            false,
        );
        let g = self.module.add_global(
            object.get_type(),
            None,
            &format!("fun.string{}", self.strings.len()),
        );
        g.set_linkage(Linkage::Private);
        g.set_constant(true);
        g.set_alignment(8);
        g.set_initializer(&object);
        let word = g.as_pointer_value().const_to_int(self.word);
        self.strings.insert(s.into(), word);
        word
    }
    /// Keep static string words in a retained data root table. Strings themselves are read-only.
    pub(super) fn string_roots(&self) {
        if self.strings.is_empty() {
            return;
        }
        let words = self.strings.values().copied().collect::<Vec<_>>();
        let array = self.word.const_array(&words);
        let roots = self.module.add_global(
            array.get_type(),
            None,
            "fun.string_roots",
        );
        roots.set_linkage(Linkage::Internal);
        roots.set_initializer(&array);
        roots.set_alignment(8);
        let ptr = self.context.ptr_type(AddressSpace::default());
        let used = ptr.const_array(&[roots.as_pointer_value()]);
        let keep =
            self.module.add_global(used.get_type(), None, "llvm.used");
        keep.set_linkage(Linkage::Appending);
        keep.set_section(Some("llvm.metadata"));
        keep.set_initializer(&used);
    }
    pub(super) fn literal(&mut self, l: &Lit) -> Result<IntValue<'ctx>> {
        Ok(match l {
            Lit::Int(n) => self.n(layout::integer(*n)),
            Lit::Char(c) => self.n(layout::integer(*c as i64)),
            Lit::String(s) => self.string(s),
            Lit::Double(bits) => {
                self.allocation(layout::DOUBLE, 0, &[self.n(*bits)])?
            }
        })
    }
    pub(super) fn atom(
        &mut self,
        a: &Atom,
        env: &Env<'ctx>,
    ) -> Result<Loc<'ctx>> {
        Ok(match a {
            Atom::Lit(Lit::Int(n)) => Loc::Int(self.n(*n as u64)),
            Atom::Lit(Lit::Double(bits)) => Loc::Double(
                self.context.f64_type().const_float(f64::from_bits(*bits)),
            ),
            Atom::Lit(l) => Loc::Word(self.literal(l)?),
            Atom::Var(v) => env[v],
            Atom::Global(v) if self.copying => {
                Loc::Root(self.globals[v].as_pointer_value())
            }
            Atom::Global(v) => Loc::Word(
                self.builder
                    .build_load(
                        self.word,
                        self.globals[v].as_pointer_value(),
                        "global",
                    )?
                    .into_int_value(),
            ),
        })
    }
    pub(super) fn loc_word(
        &self,
        value: Loc<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        match value {
            Loc::Root(p) => Ok(self
                .builder
                .build_load(self.word, p, "root")?
                .into_int_value()),
            Loc::Word(v) => Ok(v),
            Loc::Int(v) => self.retag(v),
            Loc::Double(v) => self.box_double(v),
        }
    }
    pub(super) fn loc_int(
        &self,
        value: Loc<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        match value {
            Loc::Root(_) => self.untag(self.loc_word(value)?),
            Loc::Word(v) => self.untag(v),
            Loc::Int(v) => Ok(v),
            Loc::Double(_) => {
                unreachable!("numeric evidence checked by Flat lint")
            }
        }
    }
    pub(super) fn loc_double(
        &self,
        value: Loc<'ctx>,
    ) -> Result<inkwell::values::FloatValue<'ctx>> {
        match value {
            Loc::Root(_) => self.unbox_double(self.loc_word(value)?),
            Loc::Word(v) => self.unbox_double(v),
            Loc::Double(v) => Ok(v),
            Loc::Int(_) => {
                unreachable!("numeric evidence checked by Flat lint")
            }
        }
    }
    pub(super) fn atom_word(
        &mut self,
        a: &Atom,
        env: &Env<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        let loc = self.atom(a, env)?;
        self.loc_word(loc)
    }
    pub(super) fn atoms(
        &mut self,
        args: &[Atom],
        env: &Env<'ctx>,
    ) -> Result<Vec<IntValue<'ctx>>> {
        let args = args
            .iter()
            .map(|a| self.atom(a, env))
            .collect::<Result<Vec<_>>>()?;
        self.loc_words(&args)
    }
    pub(super) fn known(
        &self,
        f: FuncId,
        env: Option<IntValue<'ctx>>,
        args: &[IntValue<'ctx>],
        tail: bool,
    ) -> Result<IntValue<'ctx>> {
        let mut actual: Vec<BasicMetadataValueEnum> =
            vec![env.unwrap_or(self.n(0)).into()];
        actual
            .extend(args.iter().map(|v| BasicMetadataValueEnum::from(*v)));
        let call = self.builder.build_call(
            self.fast[f.0 as usize],
            &actual,
            "known",
        )?;
        call.set_call_convention(TAIL_CC);
        call.set_tail_call(tail);
        if self
            .builder
            .get_insert_block()
            .unwrap()
            .get_parent()
            .unwrap()
            .get_call_conventions()
            != TAIL_CC
        {
            call.set_tail_call_kind(
                inkwell::values::LLVMTailCallKind::LLVMTailCallKindNoTail,
            );
        }
        Ok(call.try_as_basic_value().unwrap_basic().into_int_value())
    }
    pub(super) fn apply(
        &self,
        f: IntValue<'ctx>,
        args: &[IntValue<'ctx>],
    ) -> Result<IntValue<'ctx>> {
        if (1..=4).contains(&args.len()) {
            let mut actual = vec![f.into()];
            actual.extend(
                args.iter().map(|v| BasicMetadataValueEnum::from(*v)),
            );
            return self.call(&format!("rt_apply{}", args.len()), &actual);
        }
        // Allocate in the entry, never in a loop reached by a join back-edge.
        let entry = self
            .builder
            .get_insert_block()
            .unwrap()
            .get_parent()
            .unwrap()
            .get_first_basic_block()
            .unwrap();
        let alloc = self.context.create_builder();
        if let Some(first) = entry.get_first_instruction() {
            alloc.position_before(&first);
        } else {
            alloc.position_at_end(entry);
        }
        let array = alloc.build_alloca(
            self.word.array_type(args.len().max(1) as u32),
            "arguments",
        )?;
        for (i, a) in args.iter().enumerate() {
            self.builder.build_store(self.slot(array, i as u64)?, *a)?;
        }
        self.call(
            "rt_apply",
            &[f.into(), self.n(args.len() as u64).into(), array.into()],
        )
    }
    pub(super) fn box_double(
        &self,
        v: inkwell::values::FloatValue<'ctx>,
    ) -> Result<IntValue<'ctx>> {
        let bits = self
            .builder
            .build_bit_cast(v, self.word, "bits")?
            .into_int_value();
        self.allocation(layout::DOUBLE, 0, &[bits])
    }
    pub(super) fn unbox_double(
        &self,
        v: IntValue<'ctx>,
    ) -> Result<inkwell::values::FloatValue<'ctx>> {
        Ok(self
            .builder
            .build_bit_cast(
                self.load(v, 1)?,
                self.context.f64_type(),
                "double",
            )?
            .into_float_value())
    }
}
/// Storage is local to codegen. All ABI boundaries still carry tagged words.
#[derive(Clone, Copy)]
pub(super) enum Loc<'ctx> {
    Root(PointerValue<'ctx>),
    Word(IntValue<'ctx>),
    Int(IntValue<'ctx>),
    Double(inkwell::values::FloatValue<'ctx>),
}
}

복사 백엔드는 수집이 일어날 수 있는 지점 뒤에 Root를 다시 읽어 들입니다. 산술 연산은 어떤 경계에서 태깅이나 박싱이 필요해질 때까지 네이티브 숫자를 레지스터에 둘 수 있습니다. 런타임 ABI가 그 경계들을 명시합니다.

부록 D: 런타임 ABI

이 부록에서는 최종 네이티브 컴파일러와 fun-rt-24를 설명합니다. 지원 호스트는 64비트 워드와 포인터를 사용하는 Apple Silicon macOS와 x86-64/AArch64 Linux입니다. 이 ABI는 이번 판의 컴파일러 내부에서만 쓰입니다. 사용자가 작성하는 FFI는 없습니다.

워드와 객체

호출 경계를 넘는 모든 소스 값은 LLVM에서는 i64, Rust에서는 u64입니다. 최하위 비트가 1이면 즉시값(immediate)을 나타냅니다. 최하위 비트가 0이면 정렬된 객체 주소를 나타냅니다. 단, 초기화 중에 임시로 쓰이는 0 자리 표시자는 예외입니다.

값인코딩
Int(n << 1) | 1, 부호 있는 63비트 2의 보수
Char왼쪽으로 1비트 시프트하고 최하위 비트를 1로 설정한 유니코드 스칼라
인자 없는 생성자(tag << 1) | 1
힙 값8바이트로 정렬된 헤더의 주소

해석은 기대 타입에 따라 정해집니다. 따라서 False, [], Nothing, ()는 모두 워드 값 1을 가질 수 있지만 서로 바꿔 쓸 수는 없습니다.

header:  [ payload size: 32 bits | constructor tag: 24 bits | kind: 8 bits ]
object:  [ header ][ payload word 0 ][ payload word 1 ] ...

크기는 페이로드 워드의 개수입니다. 생성자 태그는 각 데이터 타입 안에서만 유효하며 선언 순서를 따릅니다. 고정된 Prelude 태그는 False=0, True=1, []=0, (:)=1, Nothing=0, Just=1, ()=0, 그리고 LT=0, EQ=1, GT=2입니다.

종류ID페이로드추적 필드
CON0생성자 필드전부
CLOSURE1느린 코드 주소, 원시 항수, 캡처캡처
PAP2클로저 워드, 원시 인자 개수, 인자들클로저와 인자들
DOUBLE3Binary64 비트없음
STRING4원시 바이트 길이, 워드 단위로 패딩된 UTF-8 바이트없음
FORWARD5수집 중의 목적지 주소수집기 전용 표식

인자 없는 생성자에는 힙 할당이 없습니다. 따라서 이동 가능한 모든 객체에는 포워딩 주소를 담을 공간이 있습니다. 문자열은 저장 시에는 바이트 수를 세고, length 계열 프리미티브에서는 유니코드 스칼라 수를 셉니다.

진입점과 적용

진입점호출 규약과 시그니처
알려진 함수tailcc i64 f(i64 env, i64 a1, ..., i64 an)
느린 어댑터C: u64 slow(u64 closure, const u64 *args)
일반 적용C: u64 rt_apply(u64 closure, usize n, const u64 *args)
소규모 적용C: u64 rt_applyN(u64 closure, u64 a1, ..., u64 aN), N=1..4
PAP 생성C: u64 rt_make_pap(u64 closure, usize n, const u64 *args)
재귀 캡처 패치C: void rt_closure_set(u64 closure, usize index, u64 value)

느린 어댑터는 정확히 함수의 구문적 항수만큼 인자를 받습니다. 일반 적용 루틴은 PAP 인자를 평탄화하거나, 정확한 호출을 수행하거나, 남은 인자를 반환된 함수에 적용합니다. 네이티브 꼬리 전달이 보장되는 것은 알려진 꼬리 호출뿐입니다. C와 tailcc 사이를 넘나드는 호출은 ABI 어댑터 프레임을 유지합니다.

모든 인자는 엄격하며 이미 평가된 상태입니다. 원시 인자 배열은 호출이 진행되는 동안 읽을 수 있는 상태로 남아 있어야 합니다. 수집기는 배열의 값을 재배치할 수 있으므로, 지속되는 버퍼는 루트 소유자에게 속합니다. 느린 진입점은 어떤 할당보다도 먼저 인자를 불러오고 생성된 루트 프레임을 설정합니다.

할당과 루트

내보내는 함수계약
void rt_init()Boehm을 초기화하고 호출한 런타임 스레드를 등록
u64 *rt_alloc(usize words)헤더를 포함하여 스캔 대상 저장 공간을 확보
u8 *rt_alloc_atomic(usize bytes)포인터가 없는 저장 공간을 확보
void rt_use_copying()초기화 전에 단일 스레드 이동 힙을 선택
void rt_roots_push(u64 *slots, usize n)쓰기 가능하고 초기화된 생성 루트 범위를 등록
void rt_roots_pop()현재 생성 루트 범위를 제거
void rt_global_root(u64 *slot)프로세스 수명 동안 정적 전역 변수를 등록

할당은 반환하기 전에 수집을 수행할 수 있습니다. 호출자는 추가 할당을 하기 전에 새 객체 전체를 초기화합니다. 네이티브 시작 과정은 함수를 설치하거나 값을 평가하기 전에 전역 변수를 등록합니다. 정적 문자열 객체는 불변이며 이동하지 않습니다. Boehm 백엔드는 이들의 주소 테이블도 유지합니다.

Rust의 Roots와 GlobalRoot 소유자는 drop될 때까지 안정적인 버퍼를 등록합니다. 이동 수집기는 이들의 셀을 갱신합니다. 빌린 문자열 슬라이스와 루트로 등록되지 않은 포인터 워드의 복사본은 할당 이후까지 살아남아서는 안 됩니다. 프리미티브 익스포트는 결과를 할당하기 전에 문자열을 소유된 Rust 값으로 디코딩합니다.

프리미티브, 오류, 종료

모든 prim_* 연산에는 rt_prim_*라는 이름의 C 익스포트가 있습니다. 각 인자와 반환 값은 워드이며, unit 결과도 마찬가지입니다. Integer와 Double 산술 연산은 대신 인라인으로 생성될 수도 있습니다. 간단하지 않은 연산은 안전한 Rust 구현을 인터프리터와 공유합니다.

프리미티브 계열인자결과
intAdd/Sub/Mul/Quot/Rem/Div/ModInt 워드 두 개Int 워드
intNegIntInt
intEq/Lt/LeInt 워드 두 개Bool 즉시값
dblAdd/Sub/Mul/Div/Pow박싱된 Double 두 개박싱된 Double
dblNeg/Sqrt/Exp/Log/Sin/Cos박싱된 Double박싱된 Double
dblEq/Lt/Le박싱된 Double 두 개Bool 즉시값
intToDbl, dblFloor/Round/Truncate변환할 원본 수치 값변환된 수치 값
chrOrd, chrChr, chrEq/Lt문자/정수 피연산자정수/문자/Bool
strAppend/Length/Eq/Compare문자열 한 개 또는 두 개문자열/Int/Bool/Int
strUnpack, strPack문자열 또는 문자 리스트문자 리스트 또는 문자열
showInt/Double/Char/String이름에 해당하는 타입의 값 하나문자열
readInt문자열Maybe Int
putStr, getLine문자열 또는 unitUnit 또는 문자열
trace문자열 메시지, 임의의 값같은 값
error, exit문자열 또는 Int반환하지 않음

정수 결과는 2⁶³을 법으로 하여 순환합니다. 부동소수점 변환은 Int 범위로 포화되며, NaN은 0이 됩니다. 0으로 나누기, 잘못된 문자, getLine에서의 EOF, 매치 실패는 런타임 오류입니다.

내보내는 함수동작
rt_error(u64 string)stdout을 플러시하고 fun: MESSAGE를 출력한 뒤 1로 종료합니다
rt_pattern_fail(const char *file, u32 line, u32 col)소스 위치를 보고하고 1로 종료합니다
rt_div_zero()fun: divide by zero를 보고하고 1로 종료합니다
rt_exit(i32 code)stdout을 플러시하고 종료합니다

생성된 C main은 런타임을 초기화하고, 전역 값을 의존성 순서대로 평가하고, 소스의 main을 유닛 워드 1에 적용한 뒤, 성공적으로 종료합니다. 런타임 패닉은 생성된 코드를 거슬러 되감기(unwinding)하지 않고 즉시 중단(abort)합니다.

부록 E: 진단 색인

이 표는 최종 컴파일러의 오류 레지스트리에서 생성됩니다. 코드는 안정적인 식별자입니다. 메시지와 보조 레이블은 소스 프로그램에 대한 맥락을 덧붙입니다. 주 레이블은 진단을 일으킨 위치를 가리킵니다.

코드의미
E0001잘못된 토큰
E0002잘못된 구문
E0003잘못된 레이아웃
E0004잘못된 프로젝션 점
E0101바인딩되지 않은 변수
E0102중복 정의
E0103바인딩되지 않은 생성자 또는 타입
E0104중복 필드
E0110예약된 연산자의 재정의
E0111Prelude 밖의 프리미티브
E0120함수가 아닌 재귀 바인딩
E0130정의 없는 결합성
E0201타입 불일치
E0202무한 타입
E0203시그니처가 너무 일반적임
E0210인스턴스 없음
E0211잘못된 인스턴스 메서드 또는 중복 인스턴스
E0230모호한 타입
E0240레코드는 생성자를 하나만 가져야 함
E0241필드 누락
E0242중복된 필드 바인딩
E0243알 수 없는 필드
E0244타입이 필드를 선언하지 않음
E0245모호한 필드 접근
E0250Main은 IO () 타입이어야 함
E0301카인드 불일치
E0302부분 적용된 동의어
E0303순환 동의어
W0001비망라적 매치
W0002중복된 절

어휘 분석, 레이아웃, 파싱은 E00xx를 사용합니다. 이름 변환은 E01xx, 타입 검사는 E02xx, 카인드 검사는 E03xx를 사용합니다. 한 패스가 여러 진단을 보고할 수 있지만, 드라이버는 오류가 발생한 뒤에는 이후 패스를 실행하지 않습니다.

W0001은 누락되었을 수 있는 매치를 보고하며, 가능한 경우 증인(witness) 패턴을 함께 제시합니다. W0002는 도달할 수 없는 절을 식별합니다. 경고는 프로그램을 거부하지 않습니다. 실행이 실제로 도달한 누락된 매치는 런타임 오류를 일으킵니다.

런타임 오류에는 컴파일러 코드 대신 메시지가 있습니다. 런타임 오류 메시지는 fun:으로 시작하며 상태 1로 종료합니다. exitWith는 요청된 상태를 사용합니다. 컴파일러 덤프와 내부 린트 실패는 개발자 도구이지, 진단 API를 대신하는 수단이 아닙니다.

레코드 필드가 모호하면 수신자의 타입을 명시하십시오. 클래스 제약이 모호하면 제약된 타입이 결과에 드러나게 하거나 명시적으로 선택하십시오. Fun은 해소되지 않은 수치 제약에 기본 타입을 지정하지 않습니다.

Bibliography

Appel, Andrew W. 1992. Compiling with Continuations. Cambridge University Press. DOI: 10.1017/CBO9780511609619.
Boehm, Hans-Juergen, and Mark Weiser. 1988. “Garbage Collection in an Uncooperative Environment.” Software: Practice and Experience 18 (9): 807–20. DOI: 10.1002/spe.4380180902.
Cheney, C. J. 1970. “A Nonrecursive List Compacting Algorithm.” Communications of the ACM 13 (11): 677–78. DOI: 10.1145/362790.362798.
Damas, Luis, and Robin Milner. 1982. “Principal Type-Schemes for Functional Programs.” In Proceedings of the 9th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages - POPL ’82, 207–12. POPL ’82. ACM Press. DOI: 10.1145/582153.582176.
Felleisen, Matthias, and Daniel P. Friedman. 1986. “Control Operators, the SECD-Machine, and the Lambda-Calculus.” TR197. Indiana University. Online.
Flanagan, Cormac, Amr Sabry, Bruce F. Duba, and Matthias Felleisen. 1993. “The Essence of Compiling with Continuations.” In Proceedings of the ACM SIGPLAN 1993 Conference on Programming Language Design and Implementation, 237–47. PLDI93. ACM. DOI: 10.1145/155090.155113.
Girard, Jean-Yves. 1972. “Interprétation Fonctionnelle Et Élimination Des Coupures de l’arithmétique d’ordre Supérieur.” PhD thesis, Université Paris VII. Online.
Henderson, Fergus. 2002. “Accurate Garbage Collection in an Uncooperative Environment.” In Proceedings of the 3rd International Symposium on Memory Management, 150–56. ISMM02. ACM. DOI: 10.1145/512429.512449.
Jones, Mark P. 1993. “A System of Constructor Classes: Overloading and Implicit Higher-Order Polymorphism.” In Proceedings of the Conference on Functional Programming Languages and Computer Architecture, 52–61. FPCA93. ACM. DOI: 10.1145/165180.165190.
———. 1999. “Typing Haskell in Haskell.” In Haskell Workshop. Online.
Jones, Richard, Antony Hosking, and Eliot Moss. 2023. The Garbage Collection Handbook: The Art of Automatic Memory Management. 2nd ed. Chapman and Hall/CRC. DOI: 10.1201/9781003276142.
Kiselyov, Oleg. 2013. “How OCaml Type Checker Works—or What Polymorphism and Garbage Collection Have in Common.” Online.
Leroy, Xavier. 1990. “The ZINC Experiment: An Economical Implementation of the ML Language.” RT-117. INRIA. Online.
Levy, Paul Blain. 2001. “Call-by-Push-Value.” PhD thesis, Queen Mary; Westfield College, University of London. Online.
Maranget, Luc. 2007. “Warnings for Pattern Matching.” Journal of Functional Programming 17 (3): 387–421. DOI: 10.1017/s0956796807006223.
———. 2008. “Compiling Pattern Matching to Good Decision Trees.” In Proceedings of the 2008 ACM SIGPLAN Workshop on ML, 35–46. ICFP08. ACM. DOI: 10.1145/1411304.1411311.
Marlow, Simon, ed. 2010. Haskell 2010 Language Report. Online.
Marlow, Simon, and Simon Peyton Jones. 2004. “Making a Fast Curry: Push/Enter Vs. Eval/Apply for Higher-Order Languages.” In Proceedings of the Ninth ACM SIGPLAN International Conference on Functional Programming, 4–15. ICFP04. ACM. DOI: 10.1145/1016850.1016856.
Maurer, Luke, Paul Downen, Zena M. Ariola, and Simon Peyton Jones. 2017. “Compiling Without Continuations.” In Proceedings of the 38th ACM SIGPLAN Conference on Programming Language Design and Implementation, 482–94. PLDI ’17. ACM. DOI: 10.1145/3062341.3062380.
Mitchell, Neil, and Shayne Fletcher. n.d. “Record Dot Syntax.” GHC Proposal 282. Online.
Pierce, Benjamin C. 2002. Types and Programming Languages. MIT Press. Online.
Rémy, Didier. 1992. “Extension of ML Type System with a Sorted Equational Theory on Types.” RR-1766. INRIA. Online.
Reynolds, John C. 1974. “Towards a Theory of Type Structure.” In Programming Symposium, 408–25. Springer Berlin Heidelberg. DOI: 10.1007/3-540-06859-7_148.

원문 © Stephen Diehl · Write You a Haskell · 산문 CC BY-SA 4.0 · 코드 MIT
yeokja 한국어 번역 · Anthropic 사의 claude-opus-5-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다 · 이 문서는 원문을 수정한 비공식 번역본이며 산문은 같은 CC BY-SA 4.0으로 제공합니다.