Chapter 6 : Extending the Language: User-defined Operator

손지웅·2025년 7월 15일

Kaleidoscope에 사용자가 직접 자신만의 이항연산자나 단항연산자를 정의할 수 있게 하는 기능
ex) def binary |> 10 (lhs rhs); 등과 같이 |> 새로운 연산자 정의
이렇게 하면 표준 라이브러리의 상당 부분을 언어 자체로 구현

전체적인 줄거리는 다음과 같다.

Parser에서 def binary, def unary 키워드 인식 -> 연산자의 이름과 우선순위를 파싱하여 저장

  • 사용자 정의 연산자는 특별한 이름을 가진 함수 호출로 취급된다.

  • 예를 들어, myoperator|> (x, y) 라는 함수를 호출하는 것으로 여겨짐

  • 함수 호출을 위한 Codgen 로직은 이미 존재함. 따라서 파서가 연산자를 함수호출형태로 변환만 해주면 코드 생성 부분은 수정할게 많이 없음.

    연산자는 사실 함수다. 연산자는 일종의 별명

User-defined Binary Operators

Lexer 확장

enum Token {
  ...
  tok_binary = -11,
  tok_unary = -12
};

if (IdentifierStr == "binary")
  return tok_binary;
if (IdentifierStr == "unary")
  return tok_unary;

-11, -12는 tok_binary, tok_unary는 "binary" "unary" 키워드를 위한 고유 토큰 값. 마찬가지로 키워드는 음수로 지정하여 기존 문자의 아스키코드인 0~255와 겹치지 않게 구분한다.

AST 구조의 일반화

기존의 AST에서는 이항 연산자를 아스키 코드를 표현한다. 따라서 새로운 연산자도 아스키 코드로 처리한다면 별도의 확장 없이 binary@ 등을 함수 이름으로 사용할 수 있다.

ex) x@y -> binary@ 함수 호출. @은 아스키 코드 64로 기존 연산자와 똑같이 AST 트리에 저장

PrototypeAST 확장

함수(여기선 연산자 이름), 인자 정보, 연산자 여부 및 우선순위 등 함수의 기본 정보를 담고 있는 AST

두 가지 정보를 추가한다.

  • IsOperator flag : 연산자가 맞는지 아닌지
  • Precedence field : 해당 연산자의 우선순위를 저장하는 field
class PrototypeAST {
  std::string Name;
  std::vector<std::string> Args;
  bool IsOperator;
  unsigned Precedence; // 이항 연산자 우선순위

  // 생성자 및 기타 메서드...
  bool isUnaryOp() const { return IsOperator && Args.size() == 1; }
  bool isBinaryOp() const { return IsOperator && Args.size() == 2; }
  char getOperatorName() const { ... }
  unsigned getBinaryPrecedence() const { ... }
};

연산자 정의

파서가 def binary |> 5 (LHS RHS) 같은 코드를 읽으면, "아, |> 라는 새로운 연산자가 정의되었구나. 우선순위는 5이고, 인자는 LHS와 RHS 두 개를 받는구나."
이 정보를 1단계에서 만든 데이터 구조(PrototypeAST)에 깔끔하게 저장된다.

prototype ::= id '(' id* ')'
           ::= binary LETTER number? (id, id)

연산자 -> 함수 호출

컴파일러는 코드에서 a|>b를 만난다. 컴파일러는 binary |> 라는 이름의 함수를 찾아 호출한다. getFunction("binary" + "|")

기존 이항 연산자 노드에 내장 연산자가 아니면 사용자 정의 연산자를 함수 호출로 처리한다.

Value *BinaryExprAST::codegen() {
  Value *L = LHS->codegen();
  Value *R = RHS->codegen();
  if (!L || !R)
    return nullptr;

  switch (Op) {
    // 내장 연산자 처리...
    default:
      break;
  }

  // 사용자 정의 연산자: "binary" + Op로 함수 이름을 만들어 호출
  Function *F = getFunction(std::string("binary") + Op);
  Value *Ops[2] = { L, R };
  return Builder->CreateCall(F, Ops, "binop");
}

이후 연산자 우선순위 등록하여, 파서가 새로운 연산자와 우선순위를 인삭하고 기존 연산자와 동일하게 처리할 수 있게끔 한다.

우선순위

파서에 우선순위가 5라고 전달 -> 우선순위에 따라 순서 파악 가능

단항 연산자도 이항 연산자와 동일하게 확장, 사용하면 된다, 이때 하나 다른 것은 순방향으로 계산이 진행되므로 우선순위 정보가 필요 없다는 것이다.

0개의 댓글