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

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. 심화. 모듈 회수 스킴을 제안하십시오. 생성자 안의 클로저, 부분 적용, 전역 값, 캡처된 환경을 고려하십시오.

더 읽을거리


원문 © Stephen Diehl · Write You a Haskell · 산문 CC BY-SA 4.0 · 코드 MIT
yeokja 한국어 번역 · Anthropic 사의 claude-opus-5-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다 · 이 문서는 원문을 수정한 비공식 번역본이며 산문은 같은 CC BY-SA 4.0으로 제공합니다.