니름 컴파일러 내부에 정의된 각 모듈과 자료형에 대한 설명을 문서화합니다. 가장 바깥쪽인 니름 컴파일러 실행 명령어부터 입력받은 파일을 특정 타겟으로 컴파일하는 내부 구조까지 개략적으로 소개합니다.
(이 문서는 아직 작성중입니다. 다른 분들의 기여나 제안도 환영합니다.)
니름 패키지를 컴파일하기 위해서는 셸에 다음 명령어를 입력합니다.
$ 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
에 새로운 규칙을 추가해야 합니다.
파서가 니름 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
}
패키지는 metadata
와 modules
두 필드로 구성됩니다. 메타데이터는 니름 IDL과
직접적으로는 관계가 없는 패키지의 정보를 담고 있습니다.
예를 들어 파이썬 패키지에 setup.cfg나
setup()
에 이러한 메타데이터들을 담듯이 니름 패키지도
Nirum.Package.Metadata.Metadata
에 이러한 내용을 담습니다. 그래서 니름 IDL을
파이썬으로 컴파일할 때 Metadata
의 정보를 이용하여 setup()
을
생성합니다.
modules
필드는 여러 모듈을 담는 필드입니다. 모듈은 여러 타입 정의(이후에
소개할 TypeDeclaration
)가 모여서 만들어지고,
패키지는 여러 모듈이 모여서 만들어집니다.
ModuleSet
은 여러 모듈을 담는 자료형입니다. 패키지를 생성하면서 다음과 같이
담고 있는 모듈 사이의 관계를 검사합니다:
ModuleSet
에 포함된Module
이 존재하지 않는 모듈을 임포트하지는 않는지(detectMissingImports
) 검사합니다.- 순환 임포트를 하지는
않는지(
detectCircularImports
) 검사합니다.
때문에 일단 ModuleSet
값이 생성되었다면
그 안에 포함된 모듈들은 상호 순환 임포트 등의 문제가 없다는 것이 보장됩니다.
Module
은 하나의 .nrm 파일을 표현하는 하스켈 자료형입니다.
위에서 언급했듯 모듈은 여러 TypeDeclaration
으로 이루어져 있으며, 여러 타입
정의를 담기 위해 DeclarationSet
자료형을 씁니다. 즉, Module
과
ModuleSet
의 관계는 TypeDeclaration
과 DeclarationSet
의 관계와
같은 관계입니다.
추가로 Nirum.Constructs.Module.Module
의 필드를 보면 docs
필드가 있는데,
니름 IDL에서 #
혹은 @docs(docs="...")
문법으로 적은 문서를 담는 필드입니다.
DeclarationSet
은 니름에서 정의할 수 있는 모든 타입들을 담기 위해 선언된
타입입니다. TypeDeclaration
, Tag
, Field
등의 정의를 담아서 사용하며,
생성할 때에는 서로의 식별자가 겹치지는 않는지 검사하는 기능을 제공합니다.
그래서 Nirum.Constructs.DeclarationSet
에 정의된 fromList
와 union
같이
DeclarationSet
을 생성하는 함수들은 식별자가 겹치는 경우를 처리하기 위해
Either NameDuplication (DeclarationSet a)
타입의 값을 반환합니다.
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 }
...
중간 데이터로 변환된 니름 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
으로 변환하는 함수입니다.
이 문서는 아직 작성중입니다. 다른 분들의 기여나 제안도 환영합니다.