PPX란 무엇인가

이 글요 글을 참고했습니다.

  • PPX는 Pre-Processor eXtension의 약자로, 컴파일러에게 컴파일 되기 전 적용되는 전처리기입니다. 작성한 코드 자체에 대한 변형이 아닌, 코드의 AST(abstract syntax tree)를 변형시키는 역할을 합니다.

  • AST는 뭘까요? 보통 파일의 컨텐츠를 나타내는 자료구조를 뜻합니다. 위키에 따르면 컴파일러에 널리 사용되는 자료구조로, 프로그램 코드의 구조를 표현하는 속성이죠. 보통 컴파일러의 구문 분석 단계의 결과물입니다. 컴파일러가 코드를 컴파일 하기위해 코드를 특정 단위로 잘라 트리로 구성해 놓은 결과물이라고 이해하면 될까요?

  • 좀 더 구체적인 예시로, 만약 ReasonML로 GraphQL 쿼리를 작성했다면, 아마 [%graphql { QUERY }]로 query validation을 했을 것입니다. 바로 graphql ppx를 사용한 것이죠! 이걸로 쿼리는 타입 안정성(type-safe)를 보장받습니다.
  • 아직도 잘 감이 안오는데, ppx를 한번 만들어봅시다. 여기서는 간단하게 [%study]를 99로 바꿔보겠습니다. 우선 buckleScript 빌드 설정을 bsconfig.json 를 통해서 살짝 건드려봅시다.
{
    "name": "build-a-ppx"
    "ppx-flags": ["ppx_study/ppx"]
    "sources": "src"
}
  • 우리가 만들 파일은 node_modules/ppx_study 밑에 ppx_study.re로 생성되어, 나중에 바이너리로 변환되어 사용될 것입니다.

  • 이제 본격적으로 PPX를 적어봅시다. PPX는 Ast_mapper.mapper 타입의 매퍼로, 이 안에는 ParseTree data type을 위한 많은 다양한 매퍼 함수들이 레코드 형태로 있습니다. 아래와 같은 형태로요.

type mapper = {
  attribute: mapper => Parsetree.attribute => Parsetree.attribute,
  case: mapper => Parsetree.case => Parsetree.case,
  cases: mapper => list Parsetree.case => list Parsetree.case,
  /* and a lot of other mapper function types */
};
  • Ast_mapper는 이미 기본 매퍼로 Ast_mapper.default_mapper를 제공하고 있고, 이걸 상속받아서 [%study]를 42로 바꿔주도록 할 것입니다.
open Asttypes;
open Parsetree;
open Ast_mapper;

let test_mapper argv => {
  ...default_mapper, /* we extend the default_mapper */  
  /* And override the 'expr' property */
  expr: fun mapper expr =>
  /* If the expression is [%test] */
  switch expr {
    | {pexp_desc: Pexp_extension {txt: "study"} (PStr [])     [@implicit_arity]} => 
     /* Then replace by 42 */
     Ast_helper.Exp.constant (Const_int 99)
    | other => default_mapper.expr mapper other
    }
};

let () = register "ppx_study" test_mapper;
  • 작성이 끝났다면 빌드를 해봅시다~ BuckleScript를 위해 파일의 바이너리를 node_modules/packages_name/binary_file에 놓아둡시다. 이 경우엔 node_modules/ppx_study/ppx 겠죠.
// 1. Ocaml 파일을 빌드 할 시
ocamlc -o outputfile yourOcamlFile.ml

//2. ReasonML 파일을 빌드 할 시
ocamlc -pp refmt --print binary -o outputFile -impl yourReasonFile.re

//3. Common module에서 사용 가능하도록 해봅시다.
ocamlc -pp refmt  print binary -o ppx -I +compiler-libs ocamlcommon.cma -impl ppx_test.re
render: fun _ => {
    <div>
        (ReasonReact.stringToElement (string_of_int [%study]))
    <div>
}
  • 짜잔! 끝입니다. (해보고 업데이트를.. 쿨럭!)