Appendix: 커스텀 MLIR Dialect 등록
소개
Chapter 01-05에서는 MLIR의 빌트인 dialect를 사용했다:
arith: 산술 연산func: 함수 정의와 호출scf: 구조적 제어 흐름 (if/while)llvm: LLVM IR 타입과 operation
이 dialect들은 강력하지만 범용적이다. FunLang과 같은 도메인별 언어의 경우 언어의 의미를 직접 표현하는 커스텀 dialect가 유용하다.
예를 들어 FunLang 클로저를 고려해 본다:
let make_adder x =
fun y -> x + y
빌트인 dialect만 사용하면 클로저를 즉시 struct, 함수 포인터, 환경 캡처로 낮춰야 한다. 하지만 커스텀 dialect를 사용하면 이렇게 표현할 수 있다:
%closure = funlang.make_closure @lambda_body, %x : (!funlang.closure)
%result = funlang.apply %closure, %y : (i32)
높은 수준에서 의미가 명확하다. 그런 다음 낮추기 pass에서 구현 세부사항 (struct 레이아웃, malloc 호출 등)으로 점진적으로 변환한다.
이 appendix는 다음을 다룬다:
- 커스텀 dialect를 C++에서 정의하는 방법
- C API shim으로 F#에 노출하는 방법
- Phase 5에서 사용할 아키텍처
아키텍처 노트: 커스텀 dialect 등록은 Phase 5의 주제다. 이 appendix는 미리 보기와 기술적 기초를 제공한다.
C API가 커스텀 Dialect를 등록할 수 없는 이유
MLIR C API (mlir-c/IR.h)는 빌트인 dialect를 로드하는 함수를 제공한다:
// C API에 있음 - 빌트인 dialect 로드
MlirDialectHandle mlirGetDialectHandle__arith__();
void mlirDialectHandleRegisterDialect(MlirDialectHandle handle, MlirContext ctx);
하지만 새 dialect를 정의하는 함수는 없다. 커스텀 dialect 정의는 C++ 코드를 요구한다:
// C++만 가능 - 새 dialect 정의
class FunLangDialect : public mlir::Dialect {
public:
FunLangDialect(mlir::MLIRContext *context);
static constexpr llvm::StringLiteral getDialectNamespace() {
return llvm::StringLiteral("funlang");
}
// ... operation, type, attribute 정의 ...
};
왜 C API에 없나?
Dialect 정의는 C++ 클래스 상속, 템플릿, TableGen 생성 코드를 사용한다. 이것들은 C FFI 경계를 넘을 수 없다. C API는 이미 정의된 dialect의 핸들만 다룰 수 있다.
해결책: C++에서 dialect를 정의하고 등록을 위한 C API shim을 작성한다. F#은 이 shim을 P/Invoke로 호출한다.
C++ 래퍼 접근법
아키텍처:
┌─────────────────────────────────────────┐
│ F# Compiler (Compiler.fs) │
│ │
│ ctx.LoadCustomDialect("funlang") │
└────────────────┬────────────────────────┘
│ P/Invoke
▼
┌─────────────────────────────────────────┐
│ C API Shim (funlang_dialect.c) │
│ │
│ void funlangRegisterDialect(MlirContext)│
└────────────────┬────────────────────────┘
│ Call C++ API
▼
┌─────────────────────────────────────────┐
│ C++ Dialect (FunLangDialect.cpp) │
│ │
│ class FunLangDialect : public Dialect { │
│ // operation, type 정의 │
│ } │
└─────────────────────────────────────────┘
C++ dialect을 공유 라이브러리 (libFunLangDialect.so)로 컴파일하고 F#이 로드한다.
최소 커스텀 Dialect in C++
C++ 파일 funlang_dialect.cpp를 만든다:
// funlang_dialect.cpp - 최소 FunLang MLIR Dialect
#include "mlir/IR/Dialect.h"
#include "mlir/IR/MLIRContext.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/DialectRegistry.h"
#include "mlir-c/IR.h"
namespace mlir {
namespace funlang {
/// FunLang Dialect 정의
class FunLangDialect : public Dialect {
public:
/// Context에 FunLang dialect 등록
explicit FunLangDialect(MLIRContext *context)
: Dialect(getDialectNamespace(), context,
mlir::TypeID::get<FunLangDialect>()) {
// 여기서 operation, type, attribute를 등록할 것
// Phase 5에서 구현
}
/// Dialect 네임스페이스 반환 ("funlang")
static constexpr llvm::StringLiteral getDialectNamespace() {
return llvm::StringLiteral("funlang");
}
};
} // namespace funlang
} // namespace mlir
// C API shim - F#에서 호출 가능
extern "C" {
/// FunLang dialect를 MLIR context에 등록
void funlangRegisterDialect(MlirContext ctx) {
mlir::MLIRContext *context = unwrap(ctx);
mlir::DialectRegistry registry;
registry.insert<mlir::funlang::FunLangDialect>();
context->appendDialectRegistry(registry);
context->loadDialect<mlir::funlang::FunLangDialect>();
}
} // extern "C"
Line-by-line 설명:
#include "mlir/IR/Dialect.h": MLIR dialect 기본 클래스namespace mlir::funlang: 네임스페이스 충돌 방지class FunLangDialect : public Dialect: 커스텀 dialect 정의.Dialect는 MLIR 기본 클래스explicit FunLangDialect(MLIRContext *context): 생성자. Context에 dialect 등록getDialectNamespace(): Dialect 이름 반환. MLIR IR에서funlang.operation_name으로 사용됨extern "C" { ... }: C linkage - name mangling 방지, F# P/Invoke 가능void funlangRegisterDialect(MlirContext ctx): C API shim. F#이 호출할 함수unwrap(ctx): MLIR C API 유틸리티 -MlirContext(불투명 핸들)을 C++MLIRContext*로 변환registry.insert<FunLangDialect>(): Registry에 dialect 추가context->appendDialectRegistry(registry): Context에 registry 추가context->loadDialect<FunLangDialect>(): Dialect 즉시 로드 (lazy loading 아님)
설계 결정: 이 dialect는 아직 operation이나 type을 정의하지 않는다. Phase 5에서
funlang.closure,funlang.apply같은 operation을 추가할 것이다.
C++ 라이브러리 빌드
CMakeLists.txt를 작성한다:
# CMakeLists.txt - FunLang Dialect 빌드
cmake_minimum_required(VERSION 3.20)
project(FunLangDialect)
# LLVM/MLIR 찾기
find_package(MLIR REQUIRED CONFIG)
list(APPEND CMAKE_MODULE_PATH "${MLIR_CMAKE_DIR}")
include(AddLLVM)
include(AddMLIR)
# Include 디렉토리
include_directories(${MLIR_INCLUDE_DIRS})
# FunLangDialect 공유 라이브러리
add_library(FunLangDialect SHARED
funlang_dialect.cpp
)
# MLIR 라이브러리 링크
target_link_libraries(FunLangDialect
PRIVATE
MLIRIR
MLIRDialect
)
# 설치
install(TARGETS FunLangDialect
LIBRARY DESTINATION lib
)
빌드:
# CMake 설정
cmake -S . -B build \
-DMLIR_DIR=$HOME/mlir-install/lib/cmake/mlir \
-DCMAKE_BUILD_TYPE=Release
# 빌드
cmake --build build
# 설치
cmake --build build --target install
이렇게 하면 libFunLangDialect.so (Linux), libFunLangDialect.dylib (macOS), 또는 FunLangDialect.dll (Windows)가 생성된다.
F#에서 사용
MlirBindings.fs에 P/Invoke 선언 추가:
// MlirBindings.fs에 추가
module MlirNative =
// ... 기존 바인딩 ...
/// FunLang 커스텀 dialect 등록 (C++ shim 호출)
[<DllImport("FunLangDialect", CallingConvention = CallingConvention.Cdecl)>]
extern void funlangRegisterDialect(MlirContext ctx)
MlirWrapper.fs의 Context 클래스에 메서드 추가:
type Context() =
let mutable handle = MlirNative.mlirContextCreate()
let mutable disposed = false
member _.Handle = handle
/// 빌트인 dialect 로드
member _.LoadDialect(dialect: string) =
if disposed then
raise (ObjectDisposedException("Context"))
MlirStringRef.WithString dialect (fun nameRef ->
MlirNative.mlirContextGetOrLoadDialect(handle, nameRef)
|> ignore)
/// 커스텀 FunLang dialect 로드
member _.LoadFunLangDialect() =
if disposed then
raise (ObjectDisposedException("Context"))
MlirNative.funlangRegisterDialect(handle)
// ... IDisposable 구현 ...
사용:
use ctx = new Context()
ctx.LoadDialect("arith")
ctx.LoadDialect("func")
ctx.LoadFunLangDialect() // 커스텀 dialect 로드
// 이제 funlang.* operation 사용 가능 (Phase 5에서 정의)
커스텀 Operation 추가 (미리 보기)
Phase 5에서 FunLang dialect에 operation을 추가한다. 미리 보기:
TableGen 정의 (FunLangOps.td):
// FunLangOps.td - FunLang operation 정의 (TableGen)
include "mlir/IR/OpBase.td"
def FunLang_Dialect : Dialect {
let name = "funlang";
let cppNamespace = "::mlir::funlang";
}
class FunLang_Op<string mnemonic, list<Trait> traits = []>
: Op<FunLang_Dialect, mnemonic, traits>;
// funlang.make_closure operation
def FunLang_MakeClosureOp : FunLang_Op<"make_closure"> {
let summary = "Create a closure capturing environment";
let arguments = (ins
FlatSymbolRefAttr:$callee,
Variadic<AnyType>:$captured
);
let results = (outs AnyType:$result);
}
// funlang.apply operation
def FunLang_ApplyOp : FunLang_Op<"apply"> {
let summary = "Apply a closure to arguments";
let arguments = (ins
AnyType:$closure,
Variadic<AnyType>:$args
);
let results = (outs AnyType:$result);
}
생성된 C++ 코드:
TableGen은 위 정의에서 C++ 클래스를 생성한다:
// 생성됨: FunLangOps.h.inc
class MakeClosureOp : public Op<MakeClosureOp, /* traits */> {
public:
static StringRef getOperationName() { return "funlang.make_closure"; }
// ... getter/setter, verifier ...
};
class ApplyOp : public Op<ApplyOp, /* traits */> {
public:
static StringRef getOperationName() { return "funlang.apply"; }
// ... getter/setter, verifier ...
};
Dialect에 등록:
// funlang_dialect.cpp 업데이트
FunLangDialect::FunLangDialect(MLIRContext *context)
: Dialect(/*...*/) {
// Operation 등록
addOperations<
MakeClosureOp,
ApplyOp
>();
}
F#에서 사용:
// 커스텀 operation 생성 (Phase 5에서 OpBuilder 확장)
let closureOp = builder.CreateMakeClosure("lambda_body", [| xValue |], loc)
let resultOp = builder.CreateApply(closureOp, [| yValue |], loc)
생성된 MLIR IR:
%closure = funlang.make_closure @lambda_body, %x : (!funlang.closure)
%result = funlang.apply %closure, %y : (i32)
Phase 5에서 이 operation들을 scf, memref, llvm dialect로 낮추는 pass를 작성할 것이다.
커스텀 Dialect를 사용할 때 vs. 빌트인 사용
커스텀 dialect를 사용해야 하는 경우:
- 도메인별 의미: FunLang 클로저, 패턴 매칭, 리스트 cons는 커스텀 operation으로 더 명확하다
- 점진적 낮추기: 높은 수준에서 시작하여 여러 pass를 통해 낮춘다
- 최적화 기회: 커스텀 operation의 패턴 매칭 최적화 작성 가능
- 가독성:
funlang.make_closure가 15줄의llvm.call,memref.alloc,memref.store보다 이해하기 쉽다
빌트인 dialect를 사용해야 하는 경우:
- 단순한 언어: 산술과 함수만 있으면
arith+func로 충분하다 - 빠른 프로토타이핑: 커스텀 dialect는 C++ 빌드 시스템이 필요하다
- MLIR 학습: 빌트인 dialect로 시작하면 개념을 빠르게 배울 수 있다
FunLang의 경우: Phase 1-4는 빌트인 dialect를 사용한다. Phase 5는 클로저와 고급 기능을 위해 커스텀 dialect를 도입한다.
요약
이 appendix에서 다음을 배웠다:
- C API 제한: MLIR C API는 커스텀 dialect 정의를 지원하지 않는다 - C++ 필요
- C++ 래퍼 패턴: C++에서 dialect를 정의하고
extern "C"shim으로 노출 - F# 통합: P/Invoke로 shim 호출, 빌트인 dialect처럼 사용
- TableGen: Operation 정의를 위한 MLIR의 코드 생성 도구
- 점진적 낮추기: 커스텀 operation → 표준 dialect → LLVM
Phase 5 미리 보기:
- FunLang dialect 정의 (
funlang.closure,funlang.apply,funlang.match) - TableGen으로 operation 생성
- 낮추기 pass 작성 (pattern rewrite 사용)
- 이전 chapter들을 커스텀 dialect 사용으로 리팩터링
리소스:
이것으로 Phase 1이 완료되었다! Chapter 00-05와 이 appendix를 통해 MLIR 기반 컴파일러 구축을 위한 완전한 기초를 갖추었다. Phase 2에서 FunLang의 더 많은 기능을 컴파일하기 시작할 것이다.