MLIR에서 TableGen 기반의 Operation Definition Specification(ODS)로 연산과 타입·속성·프로퍼티·제약·트레이트를 정의하는 방법을 상세히 설명하고, 빌더/파서/프린터/검증/선언적 어셈블리 포맷/열거형/생성되는 C++ 코드/디버깅 팁을 다룹니다.
mlir::Op C++ 템플릿을 특수화하는 것 외에도, MLIR는 연산과 데이터 타입을 테이블 기반 방식으로 정의하는 것을 지원합니다. 이는 TableGen을 통해 달성되며, TableGen은 도메인 특화 정보의 기록을 유지하기 위한 일반적인 언어이자 툴링입니다. 연산에 관한 사실들은 간결하게 TableGen 레코드에 기술되고, 컴파일러 빌드 시점에 동일한 의미의 mlir::Op C++ 템플릿 특수화로 확장됩니다.
이 문서는 테이블 기반 방식으로 연산을 정의하기 위해 사용 가능한 모든 메커니즘을 상세히 설명합니다. 이는 튜토리얼이 아닌 명세를 목표로 합니다. 튜토리얼은 MLIR 그래프 리라이트 추가를 위한 퀵스타트를 참고하세요.
각 메커니즘을 자세히 설명함과 더불어, 본 문서는 모범 사례도 포착하려고 노력합니다. 이는 인용된 글머리표로 표시됩니다.
MLIR는 플러그 가능(plugabble)한 다이얼렉트를 허용하며, 다이얼렉트는 그 외에도 연산 리스트를 포함합니다. 이 개방적이고 확장 가능한 생태계는 “문자열 기반(stringly)” 타입 IR 문제를 유발합니다. 예를 들면, 최적화와 분석 패스 동안의 반복적인 문자열 비교, 직관적이지 않은 접근자(예: 더 일반적인 반환 타입을 갖는 제네릭/에러 유발 getOperand(3) vs 자기 문서화되는 getStride()), 기본 인자가 없는 장황한 제네릭 생성자, 장황한 텍스트 IR 덤프 등이 있습니다. 또한 연산 검증은 다음과 같습니다:
해결책은 테이블 기반 방식으로 op 정의를 지원하는 것입니다. 그러면 각 다이얼렉트마다 각 op에 대해 알아야 할 모든 것(제약, 커스텀 어셈블리 폼 등)을 담은 중앙의 장소를 가질 수 있습니다. 이 설명은 빌드, 검증, 파싱, 프린팅, 분석 등을 위한 헬퍼 함수와 클래스를 생성하는 데에도 사용됩니다.
C++ 템플릿과 비교할 때 이 테이블 기반 접근법은 다음과 같은(이에 국한되지 않는) 여러 이점을 갖습니다:
우리는 TableGen을 연산 정보를 지정하는 언어로 사용합니다. TableGen 자체는 레코드를 작성하기 위한 문법만 제공합니다. TableGen 파일(일반적으로 파일명 접미사 .td)에서 허용되는 문법과 구성은 여기에서 찾을 수 있습니다.
class는 C++ 클래스와 유사하며, 템플릿을 사용할 수 있고 상속될 수 있습니다.def는 C++ 객체와 유사하며, TableGen class를 특수화하여 선언할 수 있습니다(예: def MyDef : MyClass<...>;) 또는 완전히 독립적으로 선언할 수 있습니다(예: def MyDef;). 추가로 템플릿화하거나 상속할 수는 없습니다.dag는 원소들로 이루어진 방향성 비순환 그래프(DAG)를 위한 전용 타입입니다. dag는 하나의 연산자와 0개 이상의 인자를 갖습니다. 문법은 (operator arg0, arg1, argN)입니다. 연산자는 어떤 TableGen def든 될 수 있고, 인자는 dag 자체를 포함해 무엇이든 될 수 있습니다. (MyOp:$op_name MyArg:$arg_name)처럼 연산자와 인자 모두에 이름을 붙일 수 있습니다.TableGen이 지원하는 모든 타입과 표현식은 언어 레퍼런스를 참고하세요.
MLIR는 특수한 TableGen 백엔드: OpDefinitionsGen를 통해 연산 정의를 돕고 그 의미를 제공하는 여러 공통 구성요소를 정의합니다. 이러한 구성요소는 OpBase.td에 정의되어 있습니다. 주요한 것들은 다음과 같습니다:
Op 클래스: 연산을 정의하기 위한 핵심 구성요소입니다. 연산과 관련된 모든 사실은 다음 구성요소의 도움을 받아 이 클래스를 특수화할 때 지정됩니다.Dialect 클래스: 하나의 논리적 그룹에 속하는 연산들은 동일한 다이얼렉트에 배치됩니다. Dialect 클래스는 다이얼렉트 수준의 정보를 포함합니다.Trait 클래스 계층: 연산의 특별한 속성과 제약(연산이 부작용을 갖는지 여부, 출력이 입력과 동일한 형태를 갖는지 여부 등)을 지정하는 데 사용됩니다.ins/outs 마커: 이는 OpDefinitionsGen 백엔드에 내장된 두 개의 특수 마커입니다. 각각 피연산자/속성과 결과의 정의로 이어집니다.TypeConstraint 클래스 계층: 피연산자 또는 결과에 대한 제약을 지정하는 데 사용됩니다. 주목할 만한 하위 클래스 계층으로는 공통 C++ 타입에 대한 제약을 나타내는 Type이 있습니다.AttrConstraint 클래스 계층: 속성(attribute)에 대한 제약을 지정하는 데 사용됩니다. 주목할 만한 하위 클래스 계층으로는 공통 타입의 값을 갖는 속성에 대한 제약을 나타내는 Attr이 있습니다.Property 클래스 계층: 연산에 내재된 비-속성-백드(non-attribute-backed) 프로퍼티를 지정하는 데 사용됩니다. 이러한 프로퍼티에는 predicate 필드나 ConfinedProp 클래스를 사용해 제약을 부과할 수 있습니다. Property의 상위 클래스인 PropConstraint는 리라이트 패턴에서 프로퍼티의 제약을 설명하는 데 사용됩니다.연산은 Op 클래스를 특수화하여 필요로 하는 모든 필드에 구체적인 내용을 채움으로써 정의됩니다. 예를 들어, tf.AvgPool은 다음과 같이 정의됩니다:
def TF_AvgPoolOp : TF_Op<"AvgPool", [NoMemoryEffect]> {
let summary = "Performs average pooling on the input.";
let description = [{
Each entry in `output` is the mean of the corresponding size `ksize`
window in `value`.
}];
let arguments = (ins
TF_FpTensor:$value,
ConfinedAttr<I64ArrayAttr, [ArrayMinCount<4>]>:$ksize,
ConfinedAttr<I64ArrayAttr, [ArrayMinCount<4>]>:$strides,
TF_AnyStrAttrOf<["SAME", "VALID"]>:$padding,
DefaultValuedAttr<TF_ConvertDataFormatAttr, "NHWC">:$data_format
);
let results = (outs
TF_FpTensor:$output
);
TF_DerivedOperandTypeAttr T = TF_DerivedOperandTypeAttr<0>;
}
아래에서 필요한 모든 필드를 설명합니다. 지원되는 필드의 전체 목록은 Op 클래스의 정의를 참조하세요.
연산 이름은 MLIR 내에서 연산에 대한 고유 식별자입니다. 예를 들어 TensorFlow 다이얼렉트에서의 덧셈 연산은 tf.Add입니다. 이는 어셈블리 언어의 니모닉과 동일합니다. 텍스트 형식에서 파싱과 프린팅에 사용되며, 그래프 리라이트에서 패턴 매칭에도 사용됩니다.
전체 연산 이름은 다이얼렉트 이름과 op 이름으로 구성되며, 전자는 다이얼렉트를 통해 제공되고 후자는 Op 클래스의 두 번째 템플릿 매개변수로 제공됩니다.
이는 한 줄짜리 summary와 더 긴 사람이 읽을 수 있는 description을 포함합니다. 이들은 다이얼렉트 문서의 자동 생성을 구동하는 데 사용됩니다. 연산의 정의 본문에 제공해야 합니다:
let summary = "...";
let description = [{
...
}];
description은 Markdown 문법으로 작성해야 합니다.
문서화를 앞부분에 배치하는 것을 권장합니다. 연산을 이해하는 데 도움이 되기 때문입니다.
- 문서화는 연산 정의의 시작 부분에 배치하세요.
- 요약은 짧고 간결해야 합니다. 대문자로 시작하는 한 줄짜리 문장이고 끝 구두점은 없어야 합니다. 자세한 설명은 description에 적으세요.
인자는 세 가지 종류가 있습니다: 피연산자(operands), 속성(attributes), 프로퍼티(properties). 피연산자는 다른 op가 생성한 런타임 값입니다. 속성과 프로퍼티는 컴파일 타임에 알려진 상수 값이며, 다음 두 범주를 포함합니다:
모든 파생 속성은 Attribute로 물질화(materialize) 가능해야 합니다. 즉, 비록 물질화되지 않더라도, 속성으로 저장할 수 있어야 합니다.
프로퍼티는 속성과 유사하지만, MLIR 컨텍스트 내에 저장되지 않고 연산 내부에 인라인으로 저장된다는 점이 다릅니다.
피연산자, 속성, 프로퍼티는 ins가 선행하는 dag 타입의 arguments 안에 지정합니다:
let arguments = (ins
<type-constraint>:$<operand-name>,
...
<attr-constraint>:$<attr-name>,
...
<property>:$<property-name>,
);
여기서 <type-constraint>는 TypeConstraint 클래스 계층의 TableGen def입니다. 마찬가지로 <attr-constraint>는 AttrConstraint 클래스 계층의 TableGen def이고, <property>는 Property의 하위 클래스입니다(프로퍼티에는 predicate 필드나 ConfinedProp 하위 클래스를 사용해 제약을 부과할 수 있습니다).
피연산자와 속성의 상대적 순서에 대한 요구사항은 없습니다. 자유롭게 섞을 수 있습니다. 피연산자 자체의 상대적 순서는 중요합니다. 각 명명된 인자로부터 해당 인자를 반환하는 명명된 getter가 생성되며(속성의 경우 저장 타입에서 구성된 반환 타입, 피연산자의 경우 Value), 각 속성의 원시 값(저장된 그대로)은 변환 패스에서 더 사용자 친화적이지 않은 반환 타입이 덜 적합할 때 사용할 수 있도록 생성된 <name>Attr getter를 통해서도 접근할 수 있습니다.
모든 인자는 다음 목적을 위해 이름을 가져야 합니다:
가변 길이 피연산자를 선언하려면 해당 피연산자의 TypeConstraint를 Variadic<...>으로 감싸세요.
보통 연산은 가변 길이 피연산자가 없거나 하나만 가집니다. 후자의 경우, 동적 피연산자가 정적 가변 길이 피연산자 정의에 대응하는지 쉽게 추론할 수 있습니다. 하지만 연산이 둘 이상(옵셔널 또는 가변)의 가변 길이 피연산자를 갖는다면, 추가 정보 없이는 동적 피연산자를 어떤 정적 가변 길이 피연산자 정의에 귀속시켜야 하는지 불가능해집니다. 따라서 모든 가변 길이 피연산자가 동일한 개수의 동적 값을 갖는다는 것을 나타내기 위해 SameVariadicOperandSize 또는 AttrSizedOperandSegments 트레이트 중 하나가 필요합니다.
가변 개수의 하위 구간(sub-range)을 갖는 가변 길이 피연산자를 선언하려면 해당 피연산자의 TypeConstraint를 VariadicOfVariadic<..., "<segment-attribute-name>">으로 감싸세요.
VariadicOfVariadic의 두 번째 필드는 가변 하위 구간의 크기를 담는 DenseI32ArrayAttr 인자의 이름입니다. 이 속성은 하위 구간의 크기를 결정하거나 업데이트할 때 사용됩니다.
선택적 피연산자를 선언하려면 해당 피연산자의 TypeConstraint를 Optional<...>으로 감싸세요.
일반적으로 연산에는 선택적 피연산자가 없거나 하나만 있습니다. 후자의 경우, 동적 피연산자가 정적 피연산자 정의에 대응하는지 쉽게 추론할 수 있습니다. 하지만 연산이 둘 이상(옵셔널 또는 가변)의 가변 길이 피연산자를 갖는다면, 추가 정보 없이는 동적 피연산자를 어떤 정적 가변 길이 피연산자 정의에 귀속시켜야 하는지 불가능해집니다. 따라서 모든 가변 길이 피연산자가 동일한 개수의 동적 값을 갖는다는 것을 나타내기 위해 SameVariadicOperandSize 또는 AttrSizedOperandSegments 트레이트 중 하나가 필요합니다.
선택적 속성을 선언하려면 해당 속성의 AttrConstraint를 OptionalAttr<...>으로 감싸세요.
기본값을 갖는 속성을 선언하려면 해당 속성의 AttrConstraint를 DefaultValuedAttr<..., "...">로 감싸세요.
DefaultValuedAttr의 두 번째 매개변수는 C++ 기본값을 담은 문자열이어야 합니다. 예를 들어, 부동소수 기본값은 "0.5f"처럼 지정하고, 정수 배열 기본값은 "{1, 2, 3}"처럼 지정합니다.
생성된 연산 프린팅 함수는 속성 값이 기본값과 동일한 경우 기본값이 있는 속성을 출력하지 않습니다.
ConfinedAttr는 값 타입이 부여하는 제약을 넘어 속성에 대한 추가 제약을 모델링하는 일반 메커니즘으로 제공됩니다. ConfinedAttr를 사용해 더 원시적인 제약들을 조합해 복합적인 제약을 구성할 수 있습니다. 예를 들어 최소값이 10이어야 하는 32비트 정수 속성은 ConfinedAttr<I32Attr, [IntMinValue<10>]>로 표현할 수 있습니다.
현재 다음과 같은 원시 제약이 지원됩니다:
IntMinValue<N>: 정수 속성이 N 이상IntMaxValue<N>: 정수 속성이 N 이하IntNEQValue<N>: 정수 속성이 N과 같지 않음IntPositive: 정수 속성의 값이 양수IntNonNegative: 정수 속성의 값이 음수가 아님IntPowerOf2: 정수 속성의 값이 0보다 큰 2의 거듭제곱ArrayMinCount<N>: 배열 속성이 적어도 N개의 원소ArrayMaxCount<N>: 배열 속성이 많아도 N개의 원소ArrayCount<N>: 배열 속성이 정확히 N개의 원소DenseArrayCount<N>: dense 배열 속성이 정확히 N개의 원소DenseArrayStrictlyPositive<arrayType>: 타입 arrayType의 dense 배열 속성이 모든 원소가 양수DenseArrayStrictlyNonNegative<arrayType>: 타입 arrayType의 dense 배열 속성이 모든 원소가 음수가 아님DenseArraySorted<arrayType>: 타입 arrayType의 dense 배열 속성이 원소가 비감소 순서DenseArrayStrictlySorted<arrayType>: 타입 arrayType의 dense 배열 속성이 원소가 증가 순서IntArrayNthElemEq<I, N>: 정수 배열 속성의 I번째 원소가 N과 같음IntArrayNthElemMinValue<I, N>: 정수 배열 속성의 I번째 원소가 N 이상IntArrayNthElemMaxValue<I, N>: 정수 배열 속성의 I번째 원소가 N 이하IntArrayNthElemInRange<I, M, N>: 정수 배열 속성의 I번째 원소가 M 이상 N 이하IsNullAttr: 비어 있어야 하는 선택적 속성 지정TODO: 더 많은 원시 제약을 설계하고 구현하기
기본값을 갖는 프로퍼티를 선언하려면 DefaultValuedProp<..., "...">를 사용하세요. 프로퍼티의 저장 데이터 타입이 인터페이스 타입과 다른 경우(예: 배열 프로퍼티는 SmallVector로 저장되지만 인터페이스 타입은 ArrayRef를 사용) 세 번째 인자로 저장 타입에 해당하는 기본값을 추가하세요.
선택적 프로퍼티를 선언하려면 OptionalProp<...>를 사용하세요. 이는 기본 프로퍼티를 std::optional로 감싸고 기본값을 std::nullopt로 제공합니다.
AllAttrOf는 모두 성립해야 하는 여러 제약을 결합할 수 있게 해줍니다.
예시:
def OpAllAttrConstraint1 : TEST_Op<"all_attr_constraint_of1"> {
let arguments = (ins I64ArrayAttr:$attr);
let results = (outs I32);
}
def OpAllAttrConstraint2 : TEST_Op<"all_attr_constraint_of2"> {
let arguments = (ins I64ArrayAttr:$attr);
let results = (outs I32);
}
def Constraint0 : AttrConstraint<
CPred<"::llvm::cast<::mlir::IntegerAttr>(::llvm::cast<ArrayAttr>($_self)[0]).getInt() == 0">,
"[0] == 0">;
def Constraint1 : AttrConstraint<
CPred<"::llvm::cast<::mlir::IntegerAttr>(::llvm::cast<ArrayAttr>($_self)[1]).getInt() == 1">,
"[1] == 1">;
def : Pat<(OpAllAttrConstraint1
AllAttrOf<[Constraint0, Constraint1]>:$attr),
(OpAllAttrConstraint2 $attr)>;
연산의 리전은 region이 선행하는 dag 타입의 regions 안에 지정합니다:
let regions = (region
<region-constraint>:$<region-name>,
...
);
가변 길이 피연산자와 결과에 사용되는 Variadic 클래스와 유사하게, 리전에는 VariadicRegion<...>을 사용할 수 있습니다. 가변 길이 리전은 현재 리전 리스트의 마지막 리전으로만 지정할 수 있습니다.
피연산자와 유사하게, 결과는 outs가 선행하는 dag 타입의 results 안에 지정합니다:
let results = (outs
<type-constraint>:$<result-name>,
...
);
가변 길이 피연산자와 유사하게, 결과에도 Variadic<...>을 사용할 수 있습니다. 또한 하나의 연산에 여러 가변 길이 결과가 있는 경우 SameVariadicResultSize를 사용합니다.
터미네이터 연산의 경우, 후속 블록은 successor가 선행하는 dag 타입의 successors 안에 지정합니다:
let successors = (successor
<successor-constraint>:$<successor-name>,
...
);
가변 길이 피연산자와 결과에 사용되는 Variadic 클래스와 유사하게, 후속 블록에는 VariadicSuccessor<...>를 사용할 수 있습니다. 가변 길이 후속 블록은 현재 후속 블록 리스트의 마지막으로만 지정할 수 있습니다.
트레이트는 문법 또는 의미에 영향을 미치는 연산 속성입니다. MLIR C++는 mlir::OpTrait 네임스페이스에서 다양한 트레이트를 모델링합니다.
연산 트레이트, 인터페이스, 그리고 여러 피연산자/속성/결과가 관련된 제약은 모두 Op 클래스의 세 번째 템플릿 매개변수로 제공됩니다. 이들은 Trait 클래스에서 파생되어야 합니다. 자세한 정보는 제약 섹션을 참조하세요.
각 연산에 대해, 인자와 반환 타입에 기반한 몇 가지 빌더가 자동으로 생성됩니다. 예를 들어, 다음과 같은 op 정의가 주어졌을 때:
def MyOp : ... {
let arguments = (ins
I32:$i32_operand,
F32:$f32_operand,
...,
I32Attr:$i32_attr,
F32Attr:$f32_attr,
...
I32Prop:$i32_prop,
...
);
let results = (outs
I32:$i32_result,
F32:$f32_result,
...
);
}
다음과 같은 빌더가 생성됩니다:
// 모든 결과-타입/피연산자/프로퍼티/폐기 가능한(discardable) 속성은 하나의 집계 매개변수를 가집니다.
// `Properties`는 `MyOp`의 프로퍼티 구조체입니다.
static void build(OpBuilder &odsBuilder, OperationState &odsState,
TypeRange resultTypes,
ValueRange operands,
Properties properties,
ArrayRef<NamedAttribute> discardableAttributes = {});
// 모든 결과-타입/피연산자/속성은 각각 하나의 집계 매개변수를 가집니다.
// 고유(inherent) 프로퍼티와 폐기 가능한 속성은 `attributes` 딕셔너리 안에 함께 섞입니다.
static void build(OpBuilder &odsBuilder, OperationState &odsState,
TypeRange resultTypes,
ValueRange operands,
ArrayRef<NamedAttribute> attributes);
// 각 결과-타입/피연산자/속성은 별도의 매개변수를 가집니다.
// 속성 매개변수는 mlir::Attribute 타입입니다.
static void build(OpBuilder &odsBuilder, OperationState &odsState,
Type i32_result, Type f32_result, ...,
Value i32_operand, Value f32_operand, ...,
IntegerAttr i32_attr, FloatAttr f32_attr, ...,
int32_t i32_prop);
// 각 결과-타입/피연산자/속성은 별도의 매개변수를 가집니다.
// 속성 매개변수는 mlir::Attribute 인스턴스로 감싸지지 않은 원시 값입니다.
// (이 빌더는 항상 생성되는 것은 아닙니다. 자세한 내용은 아래 설명을 참조하세요.)
static void build(OpBuilder &odsBuilder, OperationState &odsState,
Type i32_result, Type f32_result, ...,
Value i32_operand, Value f32_operand, ...,
APInt i32_attr, StringRef f32_attr, ...,
int32_t i32_prop, ...);
// 각 피연산자/속성은 별도의 매개변수를 가지지만 결과 타입은 집계형입니다.
static void build(OpBuilder &odsBuilder, OperationState &odsState,
TypeRange resultTypes,
Value i32_operand, Value f32_operand, ...,
IntegerAttr i32_attr, FloatAttr f32_attr, ...,
int32_t i32_prop, ...);
// 모든 피연산자/속성은 집계 매개변수를 가집니다.
// 반환 타입을 추론할 수 있는 경우에 생성됩니다.
static void build(OpBuilder &odsBuilder, OperationState &odsState,
ValueRange operands,
Properties properties,
ArrayRef<NamedAttribute> discardableAttributes);
// 모든 피연산자/속성은 집계 매개변수를 가집니다.
// 반환 타입을 추론할 수 있는 경우에 생성됩니다. 레거시 병합 속성 딕셔너리를 사용합니다.
static void build(OpBuilder &odsBuilder, OperationState &odsState,
ValueRange operands, ArrayRef<NamedAttribute> attributes);
// (그리고 특정 op에 따라 수동으로 지정된 빌더들.)
첫 두 형태는 정확한 op에 관계없이 동일한 형태로 op를 생성할 수 있도록 기본적인 일관성을 제공합니다. 이는 선언적 패턴 리라이트를 구현하는 데 특히 유용합니다.
세 번째와 네 번째 형태는 시그니처를 통해 더 좋은 보장을 제공하므로 수동으로 작성된 코드에서 사용하기 좋습니다.
네 번째 형태는 op의 어떤 속성이라도 Attr.returnType이 Attr.storageType과 다르고, 감싸지지 않은 값으로부터 속성을 빌드하는 방법을 알고 있는 경우(즉, Attr.constBuilderCall이 정의된 경우) 생성됩니다. 추가로, 세 번째 형태에서 arguments 리스트의 뒤쪽에 나타나는 속성이 기본값을 가지면, 그 기본값이 선언에서 제공됩니다. 이는 현재 BoolAttr, StrAttr, EnumAttr에 대해 동작하며, 향후 목록은 늘어날 수 있습니다. 따라서 가능하다면 이 기능을 활용하기 위해 기본값이 있는 속성은 arguments 리스트의 끝에 배치하는 것이 좋습니다(이는 본질적으로 C++ 함수 매개변수 기본값 배치 제한에 기인합니다). 그렇지 않으면 세 번째 형태의 빌더는 여전히 생성되지만, arguments 리스트 끝에 있지 않은 속성에 대한 기본값은 빌더의 시그니처에 제공되지 않습니다.
ODS는 다음의 경우 반환 타입을 지정할 필요가 없는 빌더를 생성합니다:
AllTypesMatch 제약);그리고 특정 op에 따라 잠재적으로 다른 빌더들이 존재할 수 있습니다. 전체 목록은 생성된 C++ 파일을 참조하세요.
위의 경우들이 모든 요구를 만족하지 못한다면, builders 필드에서 추가 편의 빌드 메서드를 다음과 같이 정의할 수 있습니다.
def MyOp : Op<"my_op", []> {
let arguments = (ins F32Attr:$attr);
let builders = [
OpBuilder<(ins "float":$val)>
];
}
builders 필드는 Op 클래스에 추가되는 커스텀 빌더 리스트입니다. 이 예시에서는 속성 대신 부동소수 값 하나를 받는 편의 빌더를 제공합니다. ins 접두사는 ODS에서 많은 함수 선언에 공통적으로 사용되며, TableGen의 dag를 사용합니다. 이어지는 것은 타입(따옴표 문자열)과 $ 기호로 접두된 이름들의 쉼표 구분 리스트입니다. 이는 다음과 같은 빌더 메서드 선언을 생성합니다:
class MyOp : /*...*/ {
/*...*/
static void build(::mlir::OpBuilder &builder, ::mlir::OperationState &state,
float val);
};
메서드에는 두 개의 추가 선행 인자가 있다는 점에 유의하세요. 이 인자들은 연산을 구성하는 데 유용합니다. 특히, 메서드는 생성할 연산의 속성, 피연산자, 리전, 결과 타입으로 state를 채워야 합니다. builder는 타입이나 중첩 연산과 같이 Op에 속하는 IR 객체를 구성하는 데 사용할 수 있습니다. 타입과 이름은 C++ 코드에서 있는 그대로 생성되므로, 타입(C++에서 Op의 네임스페이스 기준)과 식별자로 유효한 C++ 구성이어야 합니다(예: class는 유효한 식별자가 아닙니다).
빌더의 구현은 다음과 같이 TableGen 코드 블록을 사용해 ODS에서 직접 제공할 수 있습니다.
def MyOp : Op<"my_op", []> {
let arguments = (ins F32Attr:$attr);
let builders = [
OpBuilder<(ins "float":$val), [{
$_state.addAttribute("attr", $_builder.getF32FloatAttr(val));
}]>
];
}
builder와 state 인자의 등가는 특별 변수 $_builder와 $_state로 사용할 수 있습니다. ins 부분에 나열된 명명 인자는 예: val처럼 직접 사용할 수 있습니다. 빌더의 본문은 특별 변수를 치환하여 생성되며 그 외에는 유효한 C++이어야 합니다. 코드 크기에 제한은 없지만, ODS 내에서는 짧은 빌더만 인라인으로 정의하고 더 긴 빌더 정의는 C++ 파일에 두는 것을 권장합니다.
마지막으로, 일부 인자에 기본값이 필요하다면 다음과 같이 타입을 CArg로 감싸 기본값을 정의할 수 있습니다.
def MyOp : Op<"my_op", []> {
let arguments = (ins F32Attr:$attr);
let builders = [
OpBuilder<(ins CArg<"float", "0.5f">:$val), [{
$_state.addAttribute("attr", $_builder.getF32FloatAttr(val));
}]>
];
}
생성된 코드는 C++의 요구사항에 따라 선언에는 기본값을 사용하지만 정의에는 사용하지 않습니다.
/// Header file.
class MyOp : /*...*/ {
/*...*/
static void build(::mlir::OpBuilder &builder, ::mlir::OperationState &state,
float val = 0.5f);
};
/// Source file.
MyOp::build(::mlir::OpBuilder &builder, ::mlir::OperationState &state,
float val) {
state.addAttribute("attr", builder.getF32FloatAttr(val));
}
연산의 커스텀 어셈블리 폼을 파싱하고 프린트하는 함수입니다.
제약으로 지정된 엔티티에 대해서는 검증 코드가 자동으로 생성됩니다. 추가적인 검증을 수행하려면 다음을 사용할 수 있습니다:
let hasVerifier = 1;
let hasRegionVerifier = 1;
이는 op 클래스에 LogicalResult verify()/LogicalResult verifyRegions() 메서드 선언을 생성하며, 여기에 추가적인 검증 제약을 정의할 수 있습니다. 중첩 연산에 접근해야 하는 검증의 경우, 형식이 잘못된 연산에 접근하지 않도록 hasRegionVerifier를 사용해야 합니다. 그 외의 검증은 hasVerifier로 구현할 수 있습니다. 이러한 검증 메서드의 실행 순서에 대해서는 다음 섹션을 확인하세요.
연산의 검증은 몇 가지 단계로 이뤄집니다.
verifyInvariants: 타입, 속성 등을 검증합니다.verifyTrait 또는 verifyWithRegions=0으로 표시한 다른 트레이트/인터페이스.hasVerifier=1로 표시된 커스텀 검증기.연산에 리전이 있는 경우, 두 번째 단계가 있을 수 있습니다.
verifyRegionTrait 또는 verifyWithRegions=1로 표시한 트레이트/인터페이스. 이는 검증기가 자신의 리전의 연산에 접근해야 함을 의미합니다.hasRegionVerifier=1로 표시된 커스텀 검증기.두 번째 단계는 리전의 연산들이 검증된 이후에 실행됩니다. 더 뒤쪽의 검증기는 이전 검증기가 검증한 특정 불변조건에 의존할 수 있으며 이를 다시 검증할 필요가 없습니다.
커스텀 검증기는 커스텀 연산 프린터를 사용하여 연산을 출력하는 것을 피해야 합니다. 커스텀 프린터는 출력되는 연산(때로는 그 부모 연산) 먼저 검증되기를 요구하기 때문입니다. 특히 진단을 출력할 때는 Error 심각도(severity) 수준을 사용해야 하며(기본적으로 제네릭 폼으로 연산을 출력), 낮은 심각도 수준(Note, Remark, Warning)은 피해야 합니다.
연산의 커스텀 어셈블리 폼을 연산의 피연산자, 속성 등을 매칭하는 선언적 문자열로 지정할 수 있습니다. 또한 연산을 빌드하기 위해 파싱되어야 하는 추가 정보를 표현할 수 있습니다:
def CallOp : Std_Op<"call", ...> {
let arguments = (ins FlatSymbolRefAttr:$callee, Variadic<AnyType>:$args);
let results = (outs Variadic<AnyType>);
let assemblyFormat = [{
$callee `(` $args `)` attr-dict `:` functional-type($args, results)
}];
}
이 포맷은 세 가지 구성 요소로 이루어집니다:
지시어는 선택적 인자 집합을 갖는 내장 함수의 한 종류입니다. 사용 가능한 지시어는 다음과 같습니다:
attr-dict
prop-dict가 존재하지 않는 한 속성 딕셔너리의 일부로 출력됩니다.attr-dict의 일부입니다.attr-dict-with-keyword
attributes 키워드를 접두합니다.prop-dict
prop-dict가 존재하면 attr-dict는 어떤 고유 속성도 포함하지 않습니다.custom < UserDirective > ( Params )
functional-type ( inputs , outputs )
inputs와 outputs 인자를 함수 타입으로 포맷합니다.inputs와 outputs에 대한 제약은 type 지시어의 input과 동일합니다.oilist ( keywordelements |otherKeyword elements ...)
operands
ref ( input )
custom 지시어의 매개변수로 사용될 수 있는 변수나 지시어에 대한 참조를 나타냅니다.functional-type과 custom을 제외한 어떤 지시어 또는 변수든 될 수 있습니다.regions
results
successors
type ( input )
input은 피연산자 또는 결과 변수, operands 지시어, 또는 results 지시어여야 합니다.qualified ( type_or_attribute )
type 지시어나 속성 매개변수를 감쌉니다.vector.multi_reduction 연산에는 kind 속성이 있습니다. 기본적으로 선언적 어셈블리는 vector.multi_reduction <minf>, ...로 출력하지만, 선언적 어셈블리 포맷에서 qualified($kind)를 사용하면 vector.multi_reduction #vector.kind<minf>, ...로 출력됩니다.리터럴은 키워드 또는 구두점을 ``로 감싼 것입니다.
다음은 유효한 구두점의 집합입니다:
:, ,, =, <, >, (, ), {, }, [, ], ->, ?, +, *
다음은 유효한 공백 구두점입니다:
\n,
\n 리터럴은 개행을 내보내고 연산의 시작으로 들여쓰기합니다. 예시는 아래와 같습니다:
let assemblyFormat = [{
`{` `\n` ` ` ` ` `this_is_on_a_newline` `\n` `}` attr-dict
}];
%results = my.operation {
this_is_on_a_newline
}
빈 리터럴 은 `)`/`]` 등 특정 리터럴 요소 뒤에 암묵적으로 삽입되는 공백을 제거하는 데 사용할 수 있습니다. 예를 들어, “`]`”는 포맷의 마지막 요소가 아닌 경우 출력에 `]` 뒤에 공백이 따라붙게 될 수 있습니다. 이 상황에서 “`]` ”는 뒤따르는 공백을 제거합니다.
변수는 연산 자체에 등록된 엔티티(예: 인자(속성 또는 피연산자), 리전, 결과, 후속 블록 등)입니다. 위의 CallOp 예에서 변수는 $callee와 $args입니다.
속성 변수는 해당 값 타입으로 출력되며, 값 타입이 빌드 가능(buildable)한 경우에는 그 타입이 생략됩니다.
선언적 어셈블리 포맷 명세는 연산을 포맷할 때의 일반적인 대다수 경우를 처리할 수 있도록 해 줍니다. 선언적 문법으로 지원되지 않는 형태로 연산의 일부를 지정해야 하거나 바라는 연산의 경우, 커스텀 지시어를 지정할 수 있습니다. 커스텀 지시어는 본질적으로 사용자가 C++를 사용해, 그렇지 않으면 선언적으로 지정되는 포맷의 하위 섹션을 프린트/파싱할 수 있게 해줍니다. 위의 커스텀 지시어 명세를 보면:
custom-directive ::= `custom` `<` UserDirective `>` `(` Params `)`
커스텀 지시어는 두 가지 주요 부분을 갖습니다: UserDirective와 Params. 커스텀 지시어는 포맷에 대한 C++ 코드를 생성할 때 print*와 parse* 메서드 호출로 변환됩니다. UserDirective는 이 두 호출에 접미사로 사용되는 식별자입니다. 즉, custom<MyDirective>(...)는 파서와 프린터에서 각각 parseMyDirective와 printMyDirective 호출로 이어집니다. Params는 변수(예: Attribute, Operand, Successor 등), 타입 지시어, attr-dict, C++ 코드 문자열의 임의 조합일 수 있습니다. 타입 지시어는 변수를 참조해야 하지만, 그 변수가 반드시 커스텀 지시어의 매개변수여야 하는 것은 아닙니다.
parse<UserDirective> 메서드의 인자는 첫째로 OpAsmParser에 대한 참조(OpAsmParser &), 둘째로 포맷에서 지정한 매개변수에 해당하는 출력 매개변수의 집합입니다. 선언적 매개변수에서 parse 메서드 인자로의 매핑은 아래와 같습니다:
속성 변수
<Attribute-Storage-Type>(예: Attribute) &<Attribute-Storage-Type>(예: Attribute) &피연산자 변수
OpAsmParser::UnresolvedOperand &Optional<OpAsmParser::UnresolvedOperand> &SmallVectorImpl<OpAsmParser::UnresolvedOperand> &SmallVectorImpl<SmallVector<OpAsmParser::UnresolvedOperand>> &Ref 지시어
Region &로 전달됩니다.리전 변수
Region &SmallVectorImpl<std::unique_ptr<Region>> &후속 블록 변수
Block *&SmallVectorImpl<Block *> &타입 지시어
Type &Type &SmallVectorImpl<Type> &SmallVectorImpl<SmallVector<Type>> &attr-dict 지시어: NamedAttrList &
prop-dict 지시어: OperationState &
변수가 선택적인 경우, 해당 변수가 존재할 때만 값을 지정해야 합니다. 그렇지 않으면 값은 None 또는 null로 유지되어야 합니다.
print<UserDirective> 메서드의 인자는 첫째로 OpAsmPrinter에 대한 참조(OpAsmPrinter &), 둘째로 op(예: FooOp op, 또는 Operation *op), 마지막으로 포맷에서 지정된 매개변수에 해당하는 인자 집합입니다. 선언적 매개변수에서 print 메서드 인자로의 매핑은 아래와 같습니다:
속성 변수
<Attribute-Storage-Type>(예: Attribute)<Attribute-Storage-Type>(예: Attribute)피연산자 변수
ValueValueOperandRangeOperandRangeRangeRef 지시어
Region &로 전달됩니다.리전 변수
Region &MutableArrayRef<Region>후속 블록 변수
Block *SuccessorRange타입 지시어
TypeTypeTypeRangeTypeRangeRangeattr-dict 지시어: DictionaryAttr
prop-dict 지시어: FooOp::Properties
변수가 선택적인 경우, 제공되는 값은 null일 수 있습니다. 변수가 ref를 사용해 커스텀 지시어 매개변수에서 참조되는 경우, 값 전달(by value)로 전달됩니다. print<UserDirective>에 참조된 변수는 바운드된 변수와 동일하게 전달되지만, parse<UserDirective>에 참조된 변수는 프린터와 같은 방식으로 전달됩니다.
커스텀 지시어는 매개변수로 C++ 코드 문자열을 받을 수 있습니다. 코드는 커스텀 파서와 프린터 호출에 그대로 붙여넣어지며, $_builder와 $_ctxt 치환이 적용됩니다. 커스텀 지시어를 매개변수화하기 위해 문자열 리터럴을 사용할 수 있습니다.
특정 상황에서 연산은 “선택적” 정보(예: 속성, 비어 있는 가변 길이 피연산자 집합)를 가질 수 있습니다. 이러한 상황에서 어셈블리 포맷의 일부분은 이 정보의 존재를 기반으로 optional로 표시될 수 있습니다. 선택적 그룹은 다음과 같이 정의됩니다:
optional-group: `(` then-elements `)` (`:` `(` else-elements `)`)? `?`
선택적 그룹의 요소에는 다음과 같은 요구사항이 있습니다:
then-elements의 첫 번째 요소는 속성, 리터럴, 피연산자, 프로퍼티 또는 리전 중 하나여야 합니다.
optionalParser가 정의되어 있고 기본값이 있어야 합니다.then-elements 또는 else-elements 내의 인자 변수 또는 타입 지시어 중 정확히 하나는 그룹의 앵커(anchor)로 표시되어야 합니다.
^를 붙여 앵커로 표시합니다.그룹 내에서는 리터럴, 변수, 커스텀 지시어, 타입 지시어만 유효한 요소입니다.
선택적 그룹의 예로는 가변 개수의 피연산자를 갖는 func.return이 있습니다.
def ReturnOp : ... {
let arguments = (ins Variadic<AnyType>:$operands);
// 피연산자가 한 개 이상 있는 경우에만 피연산자와 타입을 출력합니다.
let assemblyFormat = "attr-dict ($operands^ `:` type($operands))?";
}
MLIR에서 unit 속성은 가능한 값이 하나뿐이라는 점에서 특별합니다. 즉, 존재 자체로 의미를 갖습니다. 선택적 그룹을 앵커링하는 유닛 속성이 그룹의 첫 번째 요소가 아닌 경우, 유닛 속성의 존재 여부는 선택적 그룹 자체의 존재 여부와 직접적으로 연관될 수 있습니다. 따라서 이러한 상황에서 유닛 속성은 출력에 나타나지 않으며, 파싱 시 선택적 그룹의 존재를 통해 자동으로 추론됩니다.
예를 들어, 다음 연산:
def FooOp : ... {
let arguments = (ins UnitAttr:$is_read_only);
let assemblyFormat = "attr-dict (`is_read_only` $is_read_only^)?";
}
는 다음과 같이 포맷됩니다:
// 유닛 속성이 존재하는 경우:
foo.op is_read_only
// 유닛 속성이 존재하지 않는 경우:
foo.op
동일한 논리가 UnitProp에도 적용됩니다.
선택적 그룹은 “else” 요소 그룹도 지원합니다. 이는 선택적 그룹의 앵커 요소가 존재하지 않는 경우 파싱/프린트되는 요소들입니다. 메인 요소 그룹과 달리, “else” 그룹은 첫 요소에 대한 제한이 없으며, 어떤 요소도 선택적의 앵커로 동작할 수 없습니다. 예시는 아래와 같습니다:
def FooOp : ... {
let arguments = (ins UnitAttr:$foo);
let assemblyFormat = "attr-dict (`foo_is_present` $foo^):(`foo_is_absent`)?";
}
는 다음과 같이 포맷됩니다:
// `foo` 속성이 존재하는 경우:
foo.op foo_is_present
// `foo` 속성이 존재하지 않는 경우:
foo.op foo_is_absent
포맷 명세에는 반드시 준수해야 하는 요구사항이 있습니다:
operands 지시어와 함께 등장해야 합니다.regions 지시어와 함께 등장해야 합니다.successors 지시어와 함께 등장해야 합니다.type 지시어를 사용하여, 개별적으로 혹은 operands 또는 results 지시어와 함께 포맷 내에 등장해야 합니다.prop-dict 지시어가 존재해야 합니다.attr-dict 지시어는 항상 존재해야 합니다.attr-dict 다중 인스턴스, 타입, 피연산자 등)를 포함하면 안 됩니다.
* attr-dict는 개별 속성과 중복되지 않는다는 점에 유의하세요. 이러한 속성은 속성 딕셔너리를 출력할 때 단순히 생략됩니다.포맷의 요구사항 중 하나는 피연산자와 결과의 타입이 항상 존재해야 한다는 것입니다. 특정 경우에는 변수의 타입이 타입 제약 또는 사용 가능한 다른 정보를 통해 유추될 수 있습니다. 이러한 경우 해당 변수의 타입을 포맷에서 생략할 수 있습니다.
일부 타입 제약은 표현이 하나만 있어 직접 빌드 가능할 수 있습니다. 예를 들어 I32 또는 Index 타입이 그렇습니다. ODS의 타입은 builderCall 필드를 설정하거나 BuildableType 클래스를 상속하여 자신을 빌드 가능하다고 표시할 수 있습니다.
select 연산의 참, 거짓, 결과 값이 종종 동일한 타입을 갖는 등, 연산에 등록된 알려진 타입 동등 제약이 많이 있습니다. 어셈블리 포맷은 이러한 동등 제약을 검사하여 누락된 변수의 타입을 식별할 수 있습니다. 현재 지원되는 트레이트는: AllTypesMatch, TypesMatchWith, SameTypeOperands, SameOperandsAndResultType입니다.
InferTypeOpInterface를 구현하는 연산은 피연산자로부터 결과 타입을 추론할 수 있으므로, 어셈블리 포맷에서 결과 타입을 생략할 수 있습니다.
hasCanonicalizer¶이 불리언 필드는 이 연산에 대해 정준화(canonicalization) 패턴이 정의되었는지를 나타냅니다. 1이면 ::getCanonicalizationPatterns()를 정의해야 합니다.
hasCanonicalizeMethod¶이 불리언 필드가 true로 설정되면, op가 간단한 “matchAndRewrite” 스타일 정준화 패턴을 위한 canonicalize 메서드를 구현함을 나타냅니다. hasCanonicalizer가 0이면, ::getCanonicalizationPatterns()의 구현이 이 함수를 호출하도록 구현됩니다.
hasFolder¶이 불리언 필드는 이 연산에 대해 일반적인 폴딩 규칙이 정의되었는지를 나타냅니다. 1이면 ::fold()를 정의해야 합니다.
테이블 기반 op 정의의 목표 중 하나는 각 op에 필요한 로직과 메서드를 최대한 자동으로 생성하는 것입니다. 그렇다 해도 항상 장기 꼬리(long-tail) 케이스는 존재합니다. 이러한 경우에는 extraClassDeclaration을 사용할 수 있습니다. extraClassDeclaration 안의 코드는 생성된 C++ op 클래스에 그대로 복사됩니다.
extraClassDeclaration은 숙련 사용자에 의한 장기 꼬리 케이스를 위한 메커니즘임에 유의하세요. 아직 구현되지 않은 광범위하게 적용 가능한 케이스의 경우에는 인프라를 개선하는 것이 바람직합니다.
TableGen에서 많은 op들이 상속하는 기본(base) op 클래스를 정의할 때, 공통 유틸리티와 인터페이스 함수의 정의를 제공하고자 할 수 있습니다. 하지만 이러한 정의 중 많은 것들은 op의 C++ 클래스 선언에 추가되는 extraClassDeclaration에서는 바람직하지 않거나 불가능할 수 있습니다. 이러한 경우 사용자들은 extraClassDefinition을 추가하여 op의 C++ 네임스페이스 내부에서 생성된 소스 파일에 추가할 코드를 정의할 수 있습니다. 치환 $cppClass는 op의 C++ 클래스 이름으로 대체됩니다.
OpDefinitionsGen은 op 정의 명세 파일을 처리하고 해당하는 C++ 코드를 담은 두 개의 파일(하나는 선언, 다른 하나는 정의)을 생성합니다. 전자는 -gen-op-decls 커맨드 라인 옵션으로, 후자는 -gen-op-defs 옵션으로 생성됩니다.
정의 파일에는 모든 op 메서드 정의가 포함되며, GET_OP_CLASSES를 정의하여 포함하고 활성화할 수 있습니다. 각 연산에 대해 OpDefinitionsGen은 연산 클래스와 피연산자 어댑터 클래스를 생성합니다. 또한 정의된 모든 op의 쉼표 구분 리스트도 포함하며, GET_OP_LIST를 정의하여 포함하고 활성화할 수 있습니다.
각 연산에 대해, 생성된 C++ 클래스 이름은 TableGen에서 def된 심볼에서 다이얼렉트 접두사를 제거한 것입니다. 첫 번째 _가 구분자 역할을 합니다. 예를 들어 def TF_AddOp의 경우 C++ 클래스 이름은 AddOp입니다. 우리는 TF 접두사를 제거하는데, 이는 op의 스코프를 위한 것이기 때문입니다. 다른 다이얼렉트도 자체 AddOp를 정의할 수 있습니다.
생성된 C++ 클래스의 네임스페이스는 다이얼렉트의 cppNamespace 필드에서 가져옵니다. 예를 들어 다이얼렉트의 cppNamespace가 A::B라면, 그 다이얼렉트의 op는 namespace A { namespace B { ... } }에 배치됩니다. 다이얼렉트가 cppNamespace를 지정하지 않으면 다이얼렉트의 이름을 네임스페이스로 사용합니다.
이는 생성된 C++ 클래스의 정규화된 이름이 연산 이름에서 설명한 연산 이름과 정확히 일치할 필요가 없음을 의미합니다. 이는 코딩 스타일 요구사항을 만족하기 위한 유연한 명명을 허용합니다.
각 연산에 대해, 우리는 자동으로 피연산자 어댑터(operand adaptor)를 생성합니다. 이 클래스는 Value 리스트로 제공된 피연산자에 “매직” 상수 없이 접근하는 문제를 해결합니다. 피연산자 어댑터는 Value 배열에 대한 참조를 받아 연산 클래스와 동일한 이름의 메서드를 제공하여 이에 접근합니다. 예를 들어 이항 산술 연산의 경우, 첫 번째 피연산자에 접근하기 위한 .lhs(), 두 번째 피연산자에 접근하기 위한 .rhs()를 제공할 수 있습니다.
피연산자 어댑터 클래스는 연산 클래스와 동일한 네임스페이스 안에 존재하며, 연산 이름 뒤에 Adaptor가 붙은 이름을 갖습니다. 또한 op 클래스 내부에 Adaptor 별칭을 갖습니다.
피연산자 어댑터는 연산도 처리하는 함수 템플릿에서 사용할 수 있습니다:
template <typename BinaryOpTy>
std::pair<Value, Value> zip(BinaryOpTy &&op) {
return std::make_pair(op.lhs(), op.rhs());;
}
void process(AddOp op, ArrayRef<Value> newOperands) {
zip(op);
zip(Adaptor<AddOp>(newOperands));
/*...*/
}
많은 연산을 가진 큰 다이얼렉트는, 생성된 op 정의의 큰 컴파일 유닛으로 인해 C++ 컴파일 시간에 어려움을 겪을 수 있습니다. mlir-tblgen은 -gen-op-defs와 -gen-op-decls에 -op-shard-count를 전달하여 op 정의를 균등하게 분할(shard)할 수 있는 기능을 제공합니다. 이 도구는 GET_OP_DEFS_${N}(여기서 ${N}은 샤드 번호)로 나뉜 정의에 대한 단일 인클루드 파일을 생성합니다. 다이얼렉트 라이브러리에 다음과 같은 파일을 추가하여 하나의 컴파일 유닛에서 샤드를 컴파일할 수 있습니다:
#include "mlir/IR/Operation.h"
// 필요한 다른 include를 추가하세요.
// 생성된 op 정의에서 공유되는 유틸리티: 커스텀 지시어 파서,
// 프린터 등.
#include "OpUtils.h"
#define GET_OP_DEFS_0
#include "MyDialectOps.cpp.inc"
참고: 이는 다이얼렉트 라이브러리 내의 공유 유틸리티 함수들을 여러 컴파일 유닛에서 공유할 수 있도록 재구조화할 것을 요구합니다. 즉, 동일 소스 파일에 static 메서드를 정의하는 대신, 공유 헤더에 선언하고 자체 소스 파일에 정의해야 합니다.
op 등록 훅들도 샤딩됩니다. 템플릿 인스턴스화가 컴파일에 매우 오래 걸릴 수 있기 때문입니다. 연산은 다이얼렉트에서 다음과 같이 등록해야 합니다:
void MyDialect::initialize() {
registerMyDialectOperations(this);
}
CMake와 Bazel 함수가 다이얼렉트 샤딩을 더 쉽게 만듭니다. 연산 유틸리티 함수를 자체 헤더로 구성했다고 가정하고, 위와 같은 파일에서 #define 없이 다음과 같은 파일을 정의하세요:
// MyDialectOps.cpp
#include "mlir/IR/Operation.h"
#include "OpUtils.h"
#include "MyDialectOps.cpp.inc"
CMake에서는 수동 mlir_tablegen 호출을 제거하고 다음으로 대체하세요:
set(LLVM_TARGET_DEFINITIONS MyDialectOps.td)
add_sharded_ops(MyDialectOps 8) # op 정의를 8개로 샤딩
add_mlir_library(MyDialect
MyDialect.cpp
MyDialectOpDefs.cpp
${SHARDED_SRCS}
DEPENDS
MLIRTestOpsShardGen
)
이렇게 하면 MyDialectOps.cpp 소스 파일이 자동으로 복제되고 지정된 샤드 수만큼 #define이 추가됩니다.
라인 밖(out-of-line)의 op 멤버 함수(예: 검증기)는 별도의 소스 파일에서 정의하는 것이 권장됩니다. 이 예시에서는 MyDialectOpDefs.cpp라고 합니다.
Bazel에서는 -gen-op-defs와 -gen-op-decls 호출을 제거하고 다음을 추가하세요:
gentbl_sharded_ops(
name = "MyDialectOpSrcs",
hdr_out = "MyDialectOps.h.inc",
shard_count = 8,
sharder = "//mlir:mlir-src-sharder",
src_file = "MyDialectOps.cpp",
src_out = "MyDialectOps.cpp.inc",
tblgen = "//mlir:mlir-tblgen",
td_file = "MyDialectOps.td",
deps = [":MyDialectOpsTdFiles"],
)
cc_library(
name = "MyDialect",
srcs = glob(["MyDialect/*.cpp"]) + [":MyDialectOpSrcs"]
)
제약은 테이블 기반 연산 정의의 핵심 개념입니다. 연산 검증과 그래프 연산 매칭은 모두 제약을 만족하는지에 기반합니다. 따라서 연산 정의와 리라이트 규칙 명세는 제약 작성과 긴밀히 연관됩니다. 우리는 모든 제약의 공통 기반 클래스로 OpBase.td에 Constraint 클래스를 갖고 있습니다.
연산의 제약은 다양한 범위를 포괄할 수 있습니다. 즉,
각각을 단일 엔티티 제약, 다중 엔티티 제약, 트레이트라고 부릅니다.
단일 피연산자, 속성 또는 결과에 국한된 제약은 연산 인자 및 연산 결과에서 설명한 대로 해당 엔티티의 선언 위치에 지정됩니다.
공통 타입의 제약 모델링을 돕기 위해 TypeConstraint의 집합이 만들어졌습니다. 이들은 Type 하위 클래스 계층입니다. 예를 들어, 부동소수임을 제약하는 F32, 부동소수 텐서임을 제약하는 TensorOf<[F32]> 등이 있습니다.
마찬가지로, 공통 속성 종류의 제약 모델링을 돕기 위해 AttrConstraint의 집합이 만들어졌습니다. 이들은 Attr 하위 클래스 계층입니다. 예를 들어, 부동소수 속성임을 제약하는 F32Attr, 부동소수 배열 속성임을 제약하는 F32ArrayAttr 등이 있습니다.
둘 이상의 피연산자/속성/결과가 관련되는 제약은 연산에서 매우 흔합니다. 예를 들어, 피연산자와 결과 사이의 원소 타입 및 모양 관계 등이 있습니다. 이러한 제약은 연산 트레이트와 제약에서 설명한 대로 Op 클래스의 템플릿 매개변수로 지정해야 합니다.
다중 엔티티 제약은 OpBase.td에서 Trait의 하위 클래스인 PredOpTrait로 모델링됩니다. 명세를 돕기 위한 많은 제약 원시(primitive)들이 제공됩니다. 전체 목록은 OpBase.td를 참고하세요.
트레이트는 연산의 내재적 속성(부작용이 있는지, 교환법칙을 만족하는지, 터미네이터인지 등)입니다. 이러한 제약은 연산 트레이트와 제약에서 설명한 대로 Op 클래스의 템플릿 매개변수로 지정해야 합니다.
트레이트는 OpBase.td에서 Trait의 하위 클래스인 NativeTrait로 모델링됩니다. 이는 C++의 mlir::OpTrait 클래스에 의해 지원되며 이에 대응하는 C++ 클래스로 변환됩니다.
제약을 작성하려면, 해당하는 프레디킷(predicate)을 제공하고 설명적인 이름을 지정해야 합니다. Pred 클래스로 모델링되는 프레디킷은 제약을 구성하는 일꾼(horsework)입니다. 제약의 프레디킷은 일반적으로 다음 두 범주의 프레디킷을 사용해 중첩 방식으로 구축됩니다:
CPred: 원시 리프 프레디킷.And, 논리합: Or, 부정: Neg, 치환: SubstLeaves, 연결: Concat)를 사용하여 자식 프레디킷으로부터 구성된 프레디킷.CPred는 더 복잡한 프레디킷을 구성하기 위한 기초입니다. TableGen의 관점에서 “원자” 프레디킷이자 TableGen과 C++ 사이의 “인터페이스”입니다. 내부에는 이미 C++ 코드가 들어가며, 특수 플레이스홀더가 치환되는 것을 제외하면 불투명한 문자열로 취급됩니다.
불리언 값을 반환하는 어떤 C++ 코드든 CPred 내부에 넣을 수 있습니다. 표현식 평가, 함수 호출, 클래스 메서드 호출 등 무엇이든 가능합니다.
C++ 환경과의 상호작용을 돕기 위해, 이 프레디킷이 사용되는 컨텍스트에서 엔티티를 참조하기 위한 몇 가지 특수 플레이스홀더가 제공됩니다. 이들은 둘러싼 환경에 대한 “훅” 역할을 합니다. 여기에는 $_builder, $_op, $_self가 포함됩니다:
$_builder는 일반적인 빌드 메서드에 접근할 수 있도록 mlir::Builder 인스턴스로 대체됩니다.$_op는 현재 연산으로 대체되어 현재 연산의 정보에 접근할 수 있습니다.$_self는 이 프레디킷이 부착된 엔티티로 대체됩니다. 예: BoolAttr는 CPred<"isa<BoolAttr>($_self)">로 감싼 속성 제약입니다. 그러면 BoolAttr:$attr에서 $_self는 $attr로 대체됩니다. 타입 제약의 경우는 조금 특별합니다. 각 타입 정의의 제약이 자연스럽게 읽히기를 원하고 타입 제약을 피연산자/결과에 직접 부착하길 원하므로, $_self는 피연산자/결과의 타입으로 대체됩니다. 예: F32:$operand에서 F32의 $_self는 operand(...).getType()으로 확장됩니다.TODO: 특수 플레이스홀더의 선행 기호를 재고하기. 궁극적으로 피연산자/결과 $-name 참조를 허용하고 싶습니다. 이러한 $-name은 밑줄로 시작할 수 있습니다.
예를 들어, 속성 attr이 IntegerAttr인지 쓰려면, C++에서는 isa<IntegerAttr>(attr)를 호출하면 됩니다. 이 코드는 CPred로 isa<IntegerAttr>($_self)처럼 감쌀 수 있으며, $_self는 확장 시 현재 속성 attr로 대체되는 특수 플레이스홀더입니다.
더 복잡한 프레디킷의 경우, 하나의 CPred로 전체 표현을 감쌀 수도 있고, 프레디킷 결합자를 사용해 여러 개의 CPred를 결합할 수도 있습니다. 예를 들어, 속성 attr이 32비트 또는 64비트 정수인지 제약을 작성하려면 다음과 같이 쓸 수 있습니다:
And<[
CPred<"$isa<IntegerAttr>(_self)()">,
Or<[
CPred<"cast<IntegerAttr>($_self).getType().isInteger(32)">,
CPred<"cast<IntegerAttr>($_self).getType().isInteger(64)">
]>
]>
(위 예시는 익숙한 예로 CPred와 프레디킷 결합자를 사용하여 복잡한 프레디킷을 작성하는 방법을 보여주기 위한 것입니다. 정수 속성에 대해서는 구체적으로 OpBase.td에 이미 I32Attr와 I64Attr가 정의되어 있습니다. 따라서 실제로는 이를 재사용해 Or<[I32Attr.predicate, I64Attr.predicate]>처럼 작성할 수 있습니다.)
TODO: 재사용 가능한 원시 제약 라이브러리 구축
프레디킷이 CPred와 프레디킷 결합자로 작성하기에는 매우 복잡하다면, 일반 C++ 함수로 작성하고 CPred를 그 함수를 “호출”하는 방식으로 사용할 수도 있습니다. 예를 들어, 속성 attr이 어떤 특성을 갖는지 검증하려면 C++ 함수로 다음처럼 작성할 수 있습니다:
bool HasSomeProperty(Attribute attr) { ... }
그리고 다음과 같이 op를 정의합니다:
def HasSomeProperty : AttrConstraint<CPred<"HasSomeProperty($_self)">,
"has some property">;
def MyOp : Op<...> {
let arguments = (ins
...
HasSomeProperty:$attr
);
}
전체 표현을 감싼 단일 CPred로 정의할지, 여러 CPred와 프레디킷 결합자로 정의할지, 아니면 함수를 “호출”하는 단일 CPred로 정의할지에 대한 명확한 기준은 없습니다. CPred와 프레디킷 결합자로 정의하는 것이(모든 로직을 C++ 함수 뒤에 숨기는 것보다) op 정의 명세에 더 많은 정보를 노출하여 잠재적으로 더 많은 자동 생성 사례를 유도할 수 있으므로 선호됩니다. 그러나 중복을 피하기 위해 공통 프레디킷의 좋은 라이브러리가 필요하며, 이는 현재 작업 중입니다.
속성은 연산의 컴파일 타임에 알려진 상수입니다.
ODS는 C++ 속성 클래스에 대한 속성 래퍼를 제공합니다. MLIR의 코어 IR 라이브러리에는 몇 가지 공통 C++ 속성 클래스가 정의되어 있으며, 다이얼렉트별 속성 클래스를 자유롭게 정의할 수 있습니다. ODS는 이러한 속성을 TableGen에서 사용하여 연산을 정의할 수 있게 해주며, 잠재적으로 더 세밀한 제약을 둘 수 있습니다. 예: StrAttr는 StringAttr에 직접 매핑됩니다. F32Attr/F64Attr는 FloatAttr이 특정 비트 폭을 갖도록 추가적으로 요구합니다.
ODS 속성은 저장 타입(속성을 저장하는 백엔드 mlir::Attribute에 해당), 반환 타입(생성된 헬퍼 getter의 C++ 반환 타입에 해당), 그리고 내부 저장과 헬퍼 메서드 간 변환 메서드를 갖도록 정의됩니다.
선택성, 기본값 등 공통 추가 속성을 지정하기 위해 ODS 속성에 적용할 수 있는 중요한 어댑터/데코레이터/수정자가 몇 가지 있습니다:
DefaultValuedAttr: 기본값을 지정합니다.OptionalAttr: 속성을 선택적으로 지정합니다.ConfinedAttr: 속성에 추가 제약을 적용합니다.AllAttrOf: 속성에 다중 제약을 적용합니다.MLIR는 C++ 열거형을 생성할 수 있으며, 리스트에서 뽑은 값 집합을 나타내는 것과 플래그 조합을 담을 수 있는 것을 각각 IntEnum과 BitEnum 클래스를 통해 지원합니다.
IntEnum과 BitEnum 클래스는 각각 허용되는 모든 경우를 EnumCase 또는 BitEnumCase 하위 클래스를 통해 완전히 지정해야 합니다. 이를 통해 ODS는 허용되는 경우만 수락하도록 추가 검증을 생성할 수 있습니다. TableGen 열거형과 이들을 감싸는 속성 또는 프로퍼티 간 상호작용을 용이하게 하고 C++에서 더 쉽게 사용하도록 하기 위해, EnumsGen TableGen 백엔드는 몇 가지 공통 유틸리티( C++ enum class, 해당 enum class에 대한 llvm::DenseMapInfo, 문자열과의 변환 함수)를 생성할 수 있습니다. 이는 mlir-tblgen의 -gen-enum-decls 및 -gen-enum-defs 커맨드 라인 옵션으로 제어됩니다.
예를 들어 다음의 EnumAttr가 주어졌을 때:
def Case15: I32EnumCase<"Case15", 15>;
def Case20: I32EnumCase<"Case20", 20>;
def MyIntEnum: I32Enum<"MyIntEnum", "An example int enum",
[Case15, Case20]> {
let cppNamespace = "Outer::Inner";
let stringToSymbolFnName = "ConvertToEnum";
let symbolToStringFnName = "ConvertToString";
}
다음과 같은 것이 mlir-tblgen -gen-enum-decls로 생성됩니다:
namespace Outer {
namespace Inner {
// An example int enum
enum class MyIntEnum : uint32_t {
Case15 = 15,
Case20 = 20,
};
std::optional<MyIntEnum> symbolizeMyIntEnum(uint32_t);
llvm::StringRef ConvertToString(MyIntEnum);
std::optional<MyIntEnum> ConvertToEnum(llvm::StringRef);
inline constexpr unsigned getMaxEnumValForMyIntEnum() {
return 20;
}
} // namespace Inner
} // namespace Outer
namespace llvm {
template<> struct DenseMapInfo<Outer::Inner::MyIntEnum> {
using StorageInfo = llvm::DenseMapInfo<uint32_t>;
static inline Outer::Inner::MyIntEnum getEmptyKey() {
return static_cast<Outer::Inner::MyIntEnum>(StorageInfo::getEmptyKey());
}
static inline Outer::Inner::MyIntEnum getTombstoneKey() {
return static_cast<Outer::Inner::MyIntEnum>(StorageInfo::getTombstoneKey());
}
static unsigned getHashValue(const Outer::Inner::MyIntEnum &val) {
return StorageInfo::getHashValue(static_cast<uint32_t>(val));
}
static bool isEqual(const Outer::Inner::MyIntEnum &lhs, const Outer::Inner::MyIntEnum &rhs) {
return lhs == rhs;
}
};
}
다음과 같은 것이 mlir-tblgen -gen-enum-defs로 생성됩니다:
namespace Outer {
namespace Inner {
llvm::StringRef ConvertToString(MyIntEnum val) {
switch (val) {
case MyIntEnum::Case15: return "Case15";
case MyIntEnum::Case20: return "Case20";
}
return "";
}
std::optional<MyIntEnum> ConvertToEnum(llvm::StringRef str) {
return llvm::StringSwitch<std::optional<MyIntEnum>>(str)
.Case("Case15", MyIntEnum::Case15)
.Case("Case20", MyIntEnum::Case20)
.Default(std::nullopt);
}
std::optional<MyIntEnum> symbolizeMyIntEnum(uint32_t value) {
switch (value) {
case 15: return MyIntEnum::Case15;
case 20: return MyIntEnum::Case20;
default: return std::nullopt;
}
}
} // namespace Inner
} // namespace Outer
마찬가지로 다음의 BitEnumAttr 정의에 대해서:
def None: I32BitEnumCaseNone<"None">;
def Bit0: I32BitEnumCaseBit<"Bit0", 0, "tagged">;
def Bit1: I32BitEnumCaseBit<"Bit1", 1>;
def Bit2: I32BitEnumCaseBit<"Bit2", 2>;
def Bit3: I32BitEnumCaseBit<"Bit3", 3>;
def MyBitEnum: I32BitEnum<"MyBitEnum", "An example bit enum",
[None, Bit0, Bit1, Bit2, Bit3]> {
// Note: this is the default value, and is listed for illustrative purposes.
let separator = "|";
}
다음과 같은 것을 가질 수 있습니다:
// An example bit enum
enum class MyBitEnum : uint32_t {
None = 0,
Bit0 = 1,
Bit1 = 2,
Bit2 = 4,
Bit3 = 8,
};
std::optional<MyBitEnum> symbolizeMyBitEnum(uint32_t);
std::string stringifyMyBitEnum(MyBitEnum);
std::optional<MyBitEnum> symbolizeMyBitEnum(llvm::StringRef);
inline constexpr MyBitEnum operator|(MyBitEnum a, MyBitEnum b) {
return static_cast<MyBitEnum>(static_cast<uint32_t>(a) | static_cast<uint32_t>(b));
}
inline constexpr MyBitEnum operator&(MyBitEnum a, MyBitEnum b) {
return static_cast<MyBitEnum>(static_cast<uint32_t>(a) & static_cast<uint32_t>(b));
}
inline constexpr MyBitEnum operator^(MyBitEnum a, MyBitEnum b) {
return static_cast<MyBitEnum>(static_cast<uint32_t>(a) ^ static_cast<uint32_t>(b));
}
inline constexpr MyBitEnum operator~(MyBitEnum bits) {
// Ensure only bits that can be present in the enum are set
return static_cast<MyBitEnum>(~static_cast<uint32_t>(bits) & static_cast<uint32_t>(15u));
}
inline constexpr bool bitEnumContainsAll(MyBitEnum bits, MyBitEnum bit) {
return (bits & bit) == bit;
}
inline constexpr bool bitEnumContainsAny(MyBitEnum bits, MyBitEnum bit) {
return (static_cast<uint32_t>(bits) & static_cast<uint32_t>(bit)) != 0;
}
inline constexpr MyBitEnum bitEnumClear(MyBitEnum bits, MyBitEnum bit) {
return bits & ~bit;
}
inline std::string stringifyEnum(MyBitEnum enumValue) {
return stringifyMyBitEnum(enumValue);
}
template <typename EnumType>
::std::optional<EnumType> symbolizeEnum(::llvm::StringRef);
template <>
inline ::std::optional<MyBitEnum> symbolizeEnum<MyBitEnum>(::llvm::StringRef str) {
return symbolizeMyBitEnum(str);
}
namespace llvm {
template<> struct DenseMapInfo<::MyBitEnum> {
using StorageInfo = llvm::DenseMapInfo<uint32_t>;
static inline ::MyBitEnum getEmptyKey() {
return static_cast<::MyBitEnum>(StorageInfo::getEmptyKey());
}
static inline ::MyBitEnum getTombstoneKey() {
return static_cast<::MyBitEnum>(StorageInfo::getTombstoneKey());
}
static unsigned getHashValue(const ::MyBitEnum &val) {
return StorageInfo::getHashValue(static_cast<uint32_t>(val));
}
static bool isEqual(const ::MyBitEnum &lhs, const ::MyBitEnum &rhs) {
return lhs == rhs;
}
};
std::string stringifyMyBitEnum(MyBitEnum symbol) {
auto val = static_cast<uint32_t>(symbol);
assert(15u == (15u | val) && "invalid bits set in bit enum");
// Special case for all bits unset.
if (val == 0) return "None";
llvm::SmallVector<llvm::StringRef, 2> strs;
if (1u == (1u & val)) { strs.push_back("tagged"); }
if (2u == (2u & val)) { strs.push_back("Bit1"); }
if (4u == (4u & val)) { strs.push_back("Bit2"); }
if (8u == (8u & val)) { strs.push_back("Bit3"); }
return llvm::join(strs, "|");
}
std::optional<MyBitEnum> symbolizeMyBitEnum(llvm::StringRef str) {
// Special case for all bits unset.
if (str == "None") return MyBitEnum::None;
llvm::SmallVector<llvm::StringRef, 2> symbols;
str.split(symbols, "|");
uint32_t val = 0;
for (auto symbol : symbols) {
auto bit = llvm::StringSwitch<std::optional<uint32_t>>(symbol)
.Case("tagged", 1)
.Case("Bit1", 2)
.Case("Bit2", 4)
.Case("Bit3", 8)
.Default(std::nullopt);
if (bit) { val |= *bit; } else { return std::nullopt; }
}
return static_cast<MyBitEnum>(val);
}
std::optional<MyBitEnum> symbolizeMyBitEnum(uint32_t value) {
// Special case for all bits unset.
if (value == 0) return MyBitEnum::None;
if (value & ~static_cast<uint32_t>(15u)) return std::nullopt;
return static_cast<MyBitEnum>(value);
}
*Enum에서 가져온 값을 갖는 Attribute를 생성하는 메커니즘이 여러 가지 있습니다.
가장 일반적인 것은 EnumAttr 클래스를 사용하는 것입니다. 이는 EnumInfo( IntEnum 또는 BitEnum)를 매개변수로 받아 하나의 인자—열거형 값—을 보관하는 속성을 구성합니다. 이 속성은 다이얼렉트 내에 정의되며, 예를 들어 열거형 값 주변에 꺾쇠 괄호를 출력하거나 니모닉을 지정하는 등의 어셈블리 포맷을 커스터마이즈할 수 있습니다.
이전 형식은 *IntEnumAttr와 *BitEnumATtr 클래스 및 해당하는 *EnumAttrCase 클래스를 사용하는 것을 포함합니다(이들은 *EnumCase가 필요한 어디든 사용할 수 있습니다). 이러한 클래스는 자신들의 값을 비트 폭의 SignlessIntegerAttr로 저장하며, 해당 값이 열거형의 유효 범위 내에 있어야 한다는 제약을 부과합니다. genSpecializedAttr 매개변수가 설정된 경우, 저장을 위한 단순 부호 없는 정수 속성 대신 래퍼 속성을 생성하기도 합니다.
열거형은 인라인으로 저장될 수 있도록 프로퍼티로 감쌀 수 있습니다. 이는 열거형의 C++ 클래스 값이 연산의 프로퍼티 구조체의 멤버가 되게 하며, 연산의 검증기가 해당 열거형 값이 열거형에 대한 유효한 값인지 검사하게 합니다.
기본 래퍼는 EnumProp이며, 단순히 EnumInfo를 받습니다.
덜 모호한 문법(니모닉과 <>로 둘러싼 열거형을 출력)을 위해 NamedEnumProp이 제공되며, 이는 *EnumInfo와 니모닉 문자열을 받습니다. 이 문자열은 프로퍼티의 문법 일부가 됩니다.
이러한 EnumProp 타입 모두에는 *EnumPropWithAttrForm이 있으며, 이는 EnumAttr에서의 투명 업그레이드를 허용하고 필요에 따라 제네릭 폼에서 해당 속성을 유지할 수도 있습니다.
mlir-tblgen을 실행해 생성물을 확인¶TableGen 문법은 때때로 난해할 수 있습니다. 생성된 내용을 읽어보는 것은 문제를 이해하고 디버그하는 데 매우 도움이 됩니다. mlir-tblgen을 빌드하려면, 빌드 디렉토리에서 cmake --build . --target mlir-tblgen을 실행하고 bin/ 하위 디렉토리에서 mlir-tblgen 바이너리를 찾으세요. 지원되는 모든 제너레이터는 mlir-tblgen --help로 확인할 수 있습니다. 예를 들어, 생성되는 C++ 코드에서 설명한 --gen-op-decls와 --gen-op-defs가 있습니다.
생성된 코드를 보려면 -I를 통해 include 경로를 제공하여 특정 제너레이터로 mlir-tblgen을 호출하세요. 예를 들어,
# op C++ 클래스 선언 보기
mlir-tblgen --gen-op-decls -I /path/to/mlir/include /path/to/input/td/file
# op C++ 클래스 정의 보기
mlir-tblgen --gen-op-defs -I /path/to/mlir/include /path/to/input/td/file
# op 문서 보기
mlir-tblgen --gen-dialect-doc -I /path/to/mlir/include /path/to/input/td/file
# op 인터페이스 C++ 클래스 선언 보기
mlir-tblgen --gen-op-interface-decls -I /path/to/mlir/include /path/to/input/td/file
# op 인터페이스 C++ 클래스 정의 보기
mlir-tblgen --gen-op-interface-defs -I /path/to/mlir/include /path/to/input/td/file
# op 인터페이스 문서 보기
mlir-tblgen --gen-op-interface-doc -I /path/to/mlir/include /path/to/input/td/file
클래스/def는 Deprecate 헬퍼 클래스를 사용해 사용중단으로 표시할 수 있습니다. 예:
def OpTraitA : NativeOpTrait<"OpTraitA">, Deprecated<"use `bar` instead">;
는 OpTraitA를 사용중단으로 표시하며, mlir-tblgen은 사용중단 상태를 알리기 위해 경고(기본값) 또는 오류(-on-deprecated 플래그에 따라)를 출력할 수 있습니다.
TableGen이 생성한 C++ 엔티티(클래스, 함수, 메서드 등)는 CppDeprecated 믹스인을 사용해 사용중단으로 표시할 수 있습니다:
def MyOp : Op<MyDialect, "my.op">, CppDeprecated<"use 'your.op' instead">;
이는 TableGen의 사용중단 메커니즘과 달리, mlir-tblgen이 경고를 출력하지 않습니다. 대신, 해당 엔티티 사용 시 C++ 컴파일러가 주어진 이유와 함께 경고를 출력합니다.
더 편리한 문법을 허용하기 위해, 익명 정의로 일반적으로 사용되는 TableGen 클래스에 대한 헬퍼 클래스가 존재합니다. 현재 포함되는 것은 다음과 같습니다:
DeprecatedOpBuilder: OpBuilder 대신 사용할 수 있으며, 동일한 인자를 갖지만 첫 번째 인자로 이유를 받습니다. 예: DeprecatedOpBuilder<"use 'build' with foo instead", (ins "int":$bar)>참고: CppDeprecated 메커니즘에 대한 지원은 각 코드 생성기가 별도로 구현해야 합니다.
op 설명은 가능한 한 선언적이어야 하며, 이를 통해 다양한 도구가 이들을 사용하고 그로부터 생성된 쿼리 메서드가 이들과 함께 작동할 수 있어야 합니다. 특히 이는 트레이트, 제약, 형태(shape) 추론 정보를 쉽게 분석할 수 있는 방식으로 지정해야 함을 의미합니다(가능한 한 불투명한 C++ 함수 호출은 피하세요).
우리는 몇몇 현대 시스템의 접근법을 고려하고 바람직한 요구사항에 집중했습니다:
C++ 코드와 분리된 레지스트리를 사용해 op를 등록합니다.
op 레지스트리는 TableGen에서 정의되며, C++ 클래스와 유틸리티 함수(빌더/검증기/파서/프린터) 생성을 위해 사용됩니다.
MLIR는 정의된 op와 미정의 op 모두를 허용합니다.
op의 트레이트(예: 교환법칙)는 레지스트리에서 op와 함께 모델링됩니다.
op의 피연산자/반환 타입 제약은 레지스트리에서 op와 함께 모델링됩니다(형태 추론 논의 참조). 이는 예로 텍스트 덤프에서 최적화된 간결한 문법을 가능하게 합니다.
op의 동작은 요약과 설명과 함께 op와 함께 문서화됩니다. 설명은 마크다운으로 작성되어 다이얼렉트의 생성된 LangRef 섹션에 포함되도록 추출됩니다.
일반 어셈블리 폼의 프린팅과 파싱은 정상적으로 사용 가능하지만, “어셈블리” 문자열과 피연산자/타입의 맵핑을 보여주는 선택적 문자열 표현에서 커스텀 파서와 프린터를 지정하거나 자동 생성할 수 있습니다.
eq를 enum으로)은 파서 생성의 일부로 지원됩니다.매칭 패턴은 op 설명과 별도로 지정됩니다.
참조 구현이 op 정의와 함께 제공될 수 있습니다.
TODO: 의존하는 op의 정의가 변경될 경우에 대한 기대 사항을 문서화하기.