Skip to content

Instantly share code, notes, and snippets.

@kanghyojun
Last active August 14, 2018 09:05
Show Gist options
  • Save kanghyojun/a7b32fddd415fb77ead1914baec53b03 to your computer and use it in GitHub Desktop.
Save kanghyojun/a7b32fddd415fb77ead1914baec53b03 to your computer and use it in GitHub Desktop.
니름 인터널 문서

니름 인터널 문서

니름 컴파일러 내부에 정의된 각 모듈과 자료형에 대한 설명을 문서화합니다. 가장 바깥쪽인 니름 컴파일러 실행 명령어부터 입력받은 파일을 특정 타겟으로 컴파일하는 내부 구조까지 개략적으로 소개합니다.

(이 문서는 아직 작성중입니다. 다른 분들의 기여나 제안도 환영합니다.)

CLI

니름 패키지를 컴파일하기 위해서는 셸에 다음 명령어를 입력합니다.

$ nirum -o out -t python examples

이 명령어를 실행하는 모듈은 src/Nirum/Cli.hs에 정의되어 있습니다. 명령어를 실행하면 package.yaml 파일의 executables에 정의된 규칙에 따라 명령어 옵션이 main 함수로 전달되고, main 함수는 전달받은 옵션을 optparse-applicative 패키지를 사용해 파싱합니다.

만약 기능을 추가하거나 변경하면서 명령어 옵션을 수정하고 싶다면 Opts.Opts를 살펴보세요. Opts.Opts의 필드를 사용하여 옵션들을 파싱하기 때문에 -o와 같이 기존에 존재하는 옵션들이 어떤식으로 파싱되는지 확인할 수 있습니다.

주어진 옵션들을 파싱한 후에는 Nirum.Targets.buildPackage 함수를 실행하여 패키지를 만들고 입력 파일들을 특정 타겟 언어로 컴파일할 준비를 합니다.

만약 package.yaml 파일에 대하여 조금 더 알아보고 싶다면 hpack 저장소를 살펴보세요.

파서

입력 파일들이 들어있는 examples 디렉터리에는 니름 IDL(인터페이스 정의 언어) 파일들이 저장되어 있습니다. 니름 컴파일러는 입력받은 *.nrm 형식의 니름 IDL 파일들을 타겟 언어로 컴파일 하기 위해 CLI에서 입력값으로 넣은 파일들을 읽어서 니름 컴파일러의 중간 데이터로 변환하는 작업을 먼저 진행합니다. 변환된 중간 데이터는 이후에 compilePackage 함수를 통해 타겟 언어로 컴파일됩니다.

니름 IDL 파일은 Nirum.Parser에 정의된 문법 규칙에 의해 파싱됩니다. 이 규칙들은 Megaparsec 라이브러리가 제공하는 파싱 함수들을 이용하여 정의되어 있는데 그 형태가 BNF와 비슷하게 생겼습니다.

가령, 니름 파서는 식별자를 파싱하기 위해 다음과 같은 함수를 정의하고 있습니다.

identifier :: Parser Identifier
identifier =
    quotedIdentifier <|> bareIdentifier <?> "identifier"
  where
    bareIdentifier :: Parser Identifier
    bareIdentifier = ...
    quotedIdentifier :: Parser Identifier
    quotedIdentifier = do
        char '`'
        identifier' <- identifierRule
        char '`'
        return identifier'

BNF로 식별자 규칙을 정의해보면 다음과 같이 비슷한 형태가 보입니다.

<identifierRule> ::= <alpha> <alnum>| "_" | "-" <alnum>
<bareIdentifier> ::= ...
<quotedIdentifier> ::= ."`" <identifierRule> "`"
<identifier> ::= <bareIdentifier> | <quotedIdentifier>

물론 파서는 문법 규칙을 정의하는 작업 외에도 잘못된 문법을 검사하는 작업과 중간 데이터로 변환하는 작업 등 여러가지 일들을 하기 떄문에 아주 깔끔하게 선언적으로만 구현되어 있는 것은 아닙니다.

만약 기존 니름 IDL에 존재하지 않던 문법을 추가하고 싶다면 Nirum.Parser에 새로운 규칙을 추가해야 합니다.

Constructs

파서가 니름 IDL을 파싱하여 니름 컴파일러에서 다룰 수 있도록 하려면 중간 데이터의 자료형이 필요합니다. 이 자료형은 니름 패키지에 구현되어 있으며 원하는 타겟 언어로 컴파일하기 위해 사용됩니다.

패키지

Nirum.Targets.buildPackage는 패키지(Nirum.Package.Metadata.Package)를 만들고, Nirum.Package.Metadata.compilePackage는 패키지를 타겟 언어로 컴파일합니다. 그래서 패키지에 대해 더 자세히 알아보려면 패키지에 속한 필드가 어떤 타입인지 확인해보는 것이 좋습니다.

-- | Represents a package which consists of modules.
data Package t =
    Package { metadata :: (Eq t, Ord t, Show t, Target t) => Metadata t
            , modules :: ModuleSet
            }

패키지는 metadatamodules 두 필드로 구성됩니다. 메타데이터는 니름 IDL과 직접적으로는 관계가 없는 패키지의 정보를 담고 있습니다. 예를 들어 파이썬 패키지에 setup.cfgsetup()에 이러한 메타데이터들을 담듯이 니름 패키지도 Nirum.Package.Metadata.Metadata에 이러한 내용을 담습니다. 그래서 니름 IDL을 파이썬으로 컴파일할 때 Metadata의 정보를 이용하여 setup()을 생성합니다.

modules 필드는 여러 모듈을 담는 필드입니다. 모듈은 여러 타입 정의(이후에 소개할 TypeDeclaration)가 모여서 만들어지고, 패키지는 여러 모듈이 모여서 만들어집니다.

ModuleSet은 여러 모듈을 담는 자료형입니다. 패키지를 생성하면서 다음과 같이 담고 있는 모듈 사이의 관계를 검사합니다:

  • ModuleSet에 포함된 Module이 존재하지 않는 모듈을 임포트하지는 않는지(detectMissingImports) 검사합니다.
  • 순환 임포트를 하지는 않는지(detectCircularImports ) 검사합니다.

때문에 일단 ModuleSet 값이 생성되었다면 그 안에 포함된 모듈들은 상호 순환 임포트 등의 문제가 없다는 것이 보장됩니다.

모듈

Module은 하나의 .nrm 파일을 표현하는 하스켈 자료형입니다.

위에서 언급했듯 모듈은 여러 TypeDeclaration으로 이루어져 있으며, 여러 타입 정의를 담기 위해 DeclarationSet 자료형을 씁니다. 즉, ModuleModuleSet의 관계는 TypeDeclarationDeclarationSet의 관계와 같은 관계입니다.

추가로 Nirum.Constructs.Module.Module의 필드를 보면 docs 필드가 있는데, 니름 IDL에서 # 혹은 @docs(docs="...") 문법으로 적은 문서를 담는 필드입니다.

DeclarationSet

DeclarationSet은 니름에서 정의할 수 있는 모든 타입들을 담기 위해 선언된 타입입니다. TypeDeclaration, Tag, Field 등의 정의를 담아서 사용하며, 생성할 때에는 서로의 식별자가 겹치지는 않는지 검사하는 기능을 제공합니다.

그래서 Nirum.Constructs.DeclarationSet에 정의된 fromListunion같이 DeclarationSet을 생성하는 함수들은 식별자가 겹치는 경우를 처리하기 위해 Either NameDuplication (DeclarationSet a) 타입의 값을 반환합니다.

TypeDeclaration

TypeDeclaration은 IDL에 정의된 모든 정의들을 나타내는 데이터입니다. TypeDeclartion은 3개의 데이터 생성자로 정의할 수 있습니다.

  • TypeDeclaration: 니름에서 제공하는 대부분의 타입 정의를 표현합니다. 대표적으로 Record와 Union 같은 자료형을 표현하는 데이터 생성자와 Int64 같은 프리미티브 자료형을 표현하는 데이터 생성자가 존재합니다.
  • ServiceDeclaration: 니름 RPC 서비스 정의입니다. 서비스는 서비스의 이름과 메서드들을 정의합니다. 메서드들은 메서드가 받는 파라미터 정보를 갖고 있습니다. 니름 IDL의 service와 대응됩니다.
  • Import: 임포트 구문을 정의합니다. 니름 IDL의 import 구문과 대응됩니다.

TypeDeclaration의 필드 중 Type은 니름 IDL 문법과 1:1로 대응되는 데이터 생성자 및 필드들을 갖고 있습니다. 가령 레코드 타입은 니름 IDL에서 다음과 같이 정의할 수 있습니다:

record foo (
    int64 bar,
)

니름 IDL에 정의된 정보를 모두 담기 위해 RecordType은 다음과 같은 타입으로 정의되어 있습니다.

data Type
    = ...
    | RecordType { fields :: DeclarationSet Field }
    ...

Targets

중간 데이터로 변환된 니름 IDL 코드는 Nirum.Package.Metadata.Target을 구현하는 클래스를 사용하여 타겟 언어로 컴파일됩니다. 만약 새로운 타겟 언어를 구현하고 싶다면 Nirum.Package.Metadata.Target을 구현하는 작업이 필요한데, 타겟 언어 중 가장 많은 구현이 된 Python 타겟을 예로 들어 설명하겠습니다:

class (Eq t, Ord t, Show t, Typeable t) => Target t where
    type family CompileResult t :: *
    type family CompileError t :: *

    -- | The name of the given target e.g. @"python"@.
    targetName :: Proxy t -> TargetName

    -- | Parse the target metadata.
    parseTarget :: Table -> Either MetadataError t

    -- | Compile the package to a source tree of the target.
    compilePackage :: Package t
                   -> Map FilePath (Either (CompileError t) (CompileResult t))

    -- | Show a human-readable message from the given 'CompileError'.
    showCompileError :: t -> CompileError t -> Text

    -- | Encode the given 'CompileResult' to a 'ByteString'
    toByteString :: t -> CompileResult t -> ByteString
  • CompileResult: 중간 데이터가 컴파일되어 나오는 결과 값의 타입을 정의합니다. 파이썬 타겟의 경우 Text.Blaze.Markup 타입의 값을 반환합니다.
  • CompileError: 컴파일에 실패했을 때 사용하는 타입을 정의합니다.
  • targetName 타겟 언어의 이름을 지정합니다. 이 이름은 CLI의 -t 옵션에서 사용됩니다.
  • parseTarget: 타겟 언어에 의존적으로 정의한 메타데이터를 어떻게 파싱할 것인지 구현하는 함수입니다. 타겟 언어를 위한 별도의 메타데이터가 없다면 코드가 없을 수도 있습니다. 파이썬 타겟의 경우 파이썬 패키지 이름, 컴파일된 파이썬 패키지를 실행할 때 필요한 니름 런타임 라이브러리의 최소 필요 버전 등을 파싱하여 사용합니다.
  • compilePackage: 니름 데이터를 타겟 언어로 컴파일하는 함수입니다. 입력으로 받은 Package를 타겟 언어로 컴파일합니다.
  • showCompileError: 컴파일 에러 메시지를 사람이 알아보기 쉬운 메시지로 만드는 함수입니다. 파이썬 타겟의 경우 별다른 함수를 구현하지는 않았습니다.
  • toByteString: CompileResult를 파일에 쓰기 위해서 ByteString으로 변환하는 함수입니다.

이 문서는 아직 작성중입니다. 다른 분들의 기여나 제안도 환영합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment