문법

문법

이 섹션은 Nim의 표준 문법을 정리해 둔 곳이에요. 다만 파서가 들여쓰기를 어떻게 처리하는지는 앞서 Lexical Analysis 섹션에서 이미 다뤘으니, 그 부분은 따로 반복하지 않을게요. 여기서는 그 위에서 실제로 어떤 구문이 가능한지, 연산자들이 어떤 규칙으로 해석되는지를 중심으로 볼 거예요.

Nim은 사용자가 직접 연산자를 정의할 수 있는 언어예요. 이진 연산자(binary operator)는 무려 11가지의 서로 다른 우선순위(precedence) 레벨을 가질 수 있죠.

출처: Nim Manual

본문

결합 법칙 (Associativity)

첫 글자가 ^인 이진 연산자는 오른쪽 결합(right-associative)을 하고, 그 외의 모든 이진 연산자는 왼쪽 결합(left-associative)을 해요. 이 차이를 직접 눈으로 확인하고 싶다면 아래 예시를 봐요. 같은 ^/ 연산자라도 오른쪽에서부터 묶이면서 결과가 완전히 달라지는 걸 볼 수 있어요.

proc `^/`(x, y: float): float =
  # a right-associative division operator
  result = x / y
echo 12 ^/ 4 ^/ 8 # 24.0 (4 / 8 = 0.5, then 12 / 0.5 = 24.0)
echo 12  / 4  / 8 # 0.375 (12 / 4 = 3.0, then 3 / 8 = 0.375)

12 ^/ 4 ^/ 812 ^/ (4 ^/ 8)로 해석되어 24.0이 되지만, 일반 나눗셈 /는 왼쪽부터라 (12 / 4) / 8 = 0.375가 되는 걸 코드 주석에서 확인할 수 있어요.

우선순위 (Precedence)

단항 연산자(unary operator)는 어떤 이진 연산자보다도 항상 강하게 묶여요. 예를 들어 $a + b($a) + b로 해석되지, $(a + b)가 아니에요. 이건 $가 단항이라 a + b 전체보다 a에 먼저 붙기 때문이죠.

여기서 한 가지 더 재미있는 규칙이 있어요. 단항 연산자의 첫 글자가 @라면 이건 시길(sigil) 같은 연산자로 취급되어, primarySuffix보다도 강하게 묶여요. @x.abc(@x).abc로 해석되지만, $x.abc$(x.abc)로 해석되는 차이가 바로 그 이유예요.

키워드가 아닌 이진 연산자의 우선순위는 다음 규칙들로 결정돼요.

  • ->, ~>, =>로 끝나는 연산자를 화살표형(arrow like) 연산자라고 부르는데, 모든 연산자 중에서 가장 낮은 우선순위를 가져요.
  • 연산자가 =로 끝나면서 첫 글자가 <, >, !, =, ~, ? 중 하나가 아니라면 **할당 연산자(assignment operator)**로 분류되고, 두 번째로 낮은 우선순위를 가져요.
  • 그 외에는 첫 글자에 따라 우선순위가 정해져요.

이 규칙을 한눈에 보기 좋게 표로 정리하면 이렇게 돼요.

우선순위 레벨 연산자 첫 글자 터미널 기호
10 (가장 높음) $ ^ OP10
9 * / div mod shl shr % * % \ / OP9
8 + - `+ - ~ `
7 & & OP7
6 .. . OP6
5 == <= < >= > != in notin is isnot not of as from = < > ! OP5
4 and OP4
3 or xor OP3
2 @ : ? OP2
1 할당 연산자 (예: +=, *=) OP1
0 (가장 낮음) 화살표형 연산자 (예: ->, =>) OP0

연산자가 접두 연산자(prefix operator)로 쓰이는지도 앞에 오는 공백에 영향을 받아요. 이 파싱 방식의 변화는 0.13.0 버전부터 도입됐는데요, echo $foo가 어떻게 해석되는지 보면 바로 이해돼요.

echo $foo
# is parsed as
echo($foo)

공백은 괄호의 해석 방식까지도 바꿔요. (a, b)가 함수 호출의 인자 목록으로 읽힐지, 튜플 생성으로 읽힐지가 공백 하나로 갈리거든요.

echo(1, 2) # pass 1 and 2 to echo
echo (1, 2) # pass the tuple (1, 2) to echo

echo(1, 2)처럼 붙여 쓰면 1과 2를 echo에 전달하는 호출이 되지만, echo (1, 2)처럼 띄어 쓰면 튜플 (1, 2)를 통째로 넘기는 게 돼요. 공백이 진짜 문법의 일부인 언어라는 게 이 예시에서 체감될 거예요.

점 계열 연산자 (Dot-like operators)

문법에서의 터미널 기호는 DOTLIKEOP예요. 점 계열 연산자는 .로 시작하되 ..로는 시작하지 않는 연산자를 말해요. 예를 들면 .? 같은 것들이죠. 이들은 .과 같은 우선순위를 가지기 때문에, a.?b.ca.?(b.c)가 아니라 (a.?b).c로 해석돼요.

문법 (Grammar)

이제 실제 문법 정의를 볼 시간이에요. 문법의 시작 심볼(start symbol)은 module이에요. 아래 블록은 compiler/parser.nim에서 생성된 것으로, Nim의 전체 문법을 EBNF 형태로 담고 있어요. 처음부터 다 외우려 하기보다는, 목차처럼 펼쳐서 참고하는 용도로 보는 걸 권장할게요.

# This file is generated by compiler/parser.nim.
module = complexOrSimpleStmt ^* (';' / IND{=})
comma = ',' COMMENT?
semicolon = ';' COMMENT?
colon = ':' COMMENT?
colcom = ':' COMMENT?
operator =  OP0 | OP1 | OP2 | OP3 | OP4 | OP5 | OP6 | OP7 | OP8 | OP9
         | 'or' | 'xor' | 'and'
         | 'is' | 'isnot' | 'in' | 'notin' | 'of' | 'as' | 'from'
         | 'div' | 'mod' | 'shl' | 'shr' | 'not' | '..'
prefixOperator = operator
optInd = COMMENT? IND?
optPar = (IND{>} | IND{=})?
simpleExpr = arrowExpr (OP0 optInd arrowExpr)* pragma?
arrowExpr = assignExpr (OP1 optInd assignExpr)*
assignExpr = orExpr (OP2 optInd orExpr)*
orExpr = andExpr (OP3 optInd andExpr)*
andExpr = cmpExpr (OP4 optInd cmpExpr)*
cmpExpr = sliceExpr (OP5 optInd sliceExpr)*
sliceExpr = ampExpr (OP6 optInd ampExpr)*
ampExpr = plusExpr (OP7 optInd plusExpr)*
plusExpr = mulExpr (OP8 optInd mulExpr)*
mulExpr = dollarExpr (OP9 optInd dollarExpr)*
dollarExpr = primary (OP10 optInd primary)*
operatorB = OP0 | OP1 | OP2 | OP3 | OP4 | OP5 | OP6 | OP7 | OP8 | OP9 |
            'div' | 'mod' | 'shl' | 'shr' | 'in' | 'notin' |
            'is' | 'isnot' | 'not' | 'of' | 'as' | 'from' | '..' | 'and' | 'or' | 'xor'
symbol = '`' (KEYW|IDENT|literal|(operator|'('|')'|'['|']'|'{'|'}'|'=')+)+ '`'
       | IDENT | 'addr' | 'type' | 'static'
symbolOrKeyword = symbol | KEYW
exprColonEqExpr = expr ((':'|'=') expr
                       / doBlock extraPostExprBlock*)?
exprEqExpr = expr ('=' expr
                  / doBlock extraPostExprBlock*)?
exprList = expr ^+ comma
optionalExprList = expr ^* comma
exprColonEqExprList = exprColonEqExpr (comma exprColonEqExpr)* (comma)?
qualifiedIdent = symbol ('.' optInd symbolOrKeyword)?
setOrTableConstr = '{' ((exprColonEqExpr comma)* | ':' ) '}'
castExpr = 'cast' ('[' optInd typeDesc optPar ']' '(' optInd expr optPar ')') /
parKeyw = 'discard' | 'include' | 'if' | 'while' | 'case' | 'try'
        | 'finally' | 'except' | 'for' | 'block' | 'const' | 'let'
        | 'when' | 'var' | 'mixin'
par = '(' optInd
          ( &parKeyw (ifExpr / complexOrSimpleStmt) ^+ ';'
          | ';' (ifExpr / complexOrSimpleStmt) ^+ ';'
          | pragmaStmt
          | simpleExpr ( (doBlock extraPostExprBlock*)
                       | ('=' expr (';' (ifExpr / complexOrSimpleStmt) ^+ ';' )? )
                       | (':' expr (',' exprColonEqExpr     ^+ ',' )? ) ) )
          optPar ')'
literal = | INT_LIT | INT8_LIT | INT16_LIT | INT32_LIT | INT64_LIT
          | UINT_LIT | UINT8_LIT | UINT16_LIT | UINT32_LIT | UINT64_LIT
          | FLOAT_LIT | FLOAT32_LIT | FLOAT64_LIT
          | STR_LIT | RSTR_LIT | TRIPLESTR_LIT
          | CHAR_LIT | CUSTOM_NUMERIC_LIT
          | NIL
generalizedLit = GENERALIZED_STR_LIT | GENERALIZED_TRIPLESTR_LIT
identOrLiteral = generalizedLit | symbol | literal
               | par | arrayConstr | setOrTableConstr | tupleConstr
               | castExpr
tupleConstr = '(' optInd (exprColonEqExpr comma?)* optPar ')'
arrayConstr = '[' optInd (exprColonEqExpr comma?)* optPar ']'
primarySuffix = '(' (exprColonEqExpr comma?)* ')'
      | '.' optInd symbolOrKeyword ('[:' exprList ']' ( '(' exprColonEqExpr ')' )?)? generalizedLit?
      | DOTLIKEOP optInd symbolOrKeyword generalizedLit?
      | '[' optInd exprColonEqExprList optPar ']'
      | '{' optInd exprColonEqExprList optPar '}'
pragma = '{.' optInd (exprColonEqExpr comma?)* optPar ('.}' | '}')
identVis = symbol OPR?  # postfix position
identVisDot = symbol '.' optInd symbolOrKeyword OPR?
identWithPragma = identVis pragma?
identWithPragmaDot = identVisDot pragma?
declColonEquals = identWithPragma (comma identWithPragma)* comma?
                  (':' optInd typeDescExpr)? ('=' optInd expr)?
identColonEquals = IDENT (comma IDENT)* comma?
     (':' optInd typeDescExpr)? ('=' optInd expr)?)
tupleTypeBracket = '[' optInd (identColonEquals (comma/semicolon)?)* optPar ']'
tupleType = 'tuple' tupleTypeBracket
tupleDecl = 'tuple' (tupleTypeBracket /
    COMMENT? (IND{>} identColonEquals (IND{=} identColonEquals)*)?)
paramList = '(' declColonEquals ^* (comma/semicolon) ')'
paramListArrow = paramList? ('->' optInd typeDesc)?
paramListColon = paramList? (':' optInd typeDesc)?
doBlock = 'do' paramListArrow pragma? colcom stmt
routineExpr = ('proc' | 'func' | 'iterator') paramListColon pragma? ('=' COMMENT? stmt)?
routineType = ('proc' | 'iterator') paramListColon pragma?
forStmt = 'for' ((varTuple / identWithPragma) ^+ comma) 'in' expr colcom stmt
forExpr = forStmt
expr = (blockExpr
      | ifExpr
      | whenExpr
      | caseStmt
      | forExpr
      | tryExpr)
      / simpleExpr
simplePrimary = SIGILLIKEOP? identOrLiteral primarySuffix*
commandStart = &('`'|IDENT|literal|'cast'|'addr'|'type'|'var'|'out'|
                 'static'|'enum'|'tuple'|'object'|'proc')
primary = simplePrimary (commandStart expr (doBlock extraPostExprBlock*)?)?
        / operatorB primary
        / routineExpr
        / rawTypeDesc
        / prefixOperator primary
rawTypeDesc = (tupleType | routineType | 'enum' | 'object' |
                ('var' | 'out' | 'ref' | 'ptr' | 'distinct') typeDesc?)
                ('not' primary)?
typeDescExpr = (routineType / simpleExpr) ('not' primary)?
typeDesc = rawTypeDesc / typeDescExpr
typeDefValue = ((tupleDecl | enumDecl | objectDecl | conceptDecl |
                 ('ref' | 'ptr' | 'distinct') (tupleDecl | objectDecl))
               / (simpleExpr (exprEqExpr ^+ comma postExprBlocks?)?))
               ('not' primary)?
extraPostExprBlock = ( IND{=} doBlock
                     | IND{=} 'of' exprList ':' stmt
                     | IND{=} 'elif' expr ':' stmt
                     | IND{=} 'except' optionalExprList ':' stmt
                     | IND{=} 'finally' ':' stmt
                     | IND{=} 'else' ':' stmt )
postExprBlocks = (doBlock / ':' (extraPostExprBlock / stmt)) extraPostExprBlock*
exprStmt = simpleExpr postExprBlocks?
         / simplePrimary (exprEqExpr ^+ comma) postExprBlocks?
         / simpleExpr '=' optInd (expr postExprBlocks?)
importStmt = 'import' optInd expr
              ((comma expr)*
              / 'except' optInd (expr ^+ comma))
exportStmt = 'export' optInd expr
              ((comma expr)*
              / 'except' optInd (expr ^+ comma))
includeStmt = 'include' optInd expr ^+ comma
fromStmt = 'from' expr 'import' optInd expr (comma expr)*
returnStmt = 'return' optInd expr?
raiseStmt = 'raise' optInd expr?
yieldStmt = 'yield' optInd expr?
discardStmt = 'discard' optInd expr?
breakStmt = 'break' optInd expr?
continueStmt = 'continue' optInd expr?
condStmt = expr colcom stmt COMMENT?
           (IND{=} 'elif' expr colcom stmt)*
           (IND{=} 'else' colcom stmt)?
ifStmt = 'if' condStmt
whenStmt = 'when' condStmt
condExpr = expr colcom stmt optInd
        ('elif' expr colcom stmt optInd)*
         'else' colcom stmt
ifExpr = 'if' condExpr
whenExpr = 'when' condExpr
whileStmt = 'while' expr colcom stmt
ofBranch = 'of' exprList colcom stmt
ofBranches = ofBranch (IND{=} ofBranch)*
                      (IND{=} 'elif' expr colcom stmt)*
                      (IND{=} 'else' colcom stmt)?
caseStmt = 'case' expr ':'? COMMENT?
            (IND{>} ofBranches DED
            | IND{=} ofBranches)
tryStmt = 'try' colcom stmt &(IND{=}? 'except'|'finally')
           (IND{=}? 'except' optionalExprList colcom stmt)*
           (IND{=}? 'finally' colcom stmt)?
tryExpr = 'try' colcom stmt &(optInd 'except'|'finally')
           (optInd 'except' optionalExprList colcom stmt)*
           (optInd 'finally' colcom stmt)?
blockStmt = 'block' symbol? colcom stmt
blockExpr = 'block' symbol? colcom stmt
staticStmt = 'static' colcom stmt
deferStmt = 'defer' colcom stmt
asmStmt = 'asm' pragma? (STR_LIT | RSTR_LIT | TRIPLESTR_LIT)
genericParam = symbol (comma symbol)* (colon expr)? ('=' optInd expr)?
genericParamList = '[' optInd
  genericParam ^* (comma/semicolon) optPar ']'
pattern = '{' stmt '}'
indAndComment = (IND{>} COMMENT)? | COMMENT?
routine = optInd identVis pattern? genericParamList?
  paramListColon pragma? ('=' COMMENT? stmt)? indAndComment
commentStmt = COMMENT
section(RULE) = COMMENT? RULE / (IND{>} (RULE / COMMENT)^+IND{=} DED)
enumDecl = 'enum' optInd (symbol pragma? optInd ('=' optInd expr COMMENT?)? comma?)+
objectWhen = 'when' expr colcom objectPart COMMENT?
            ('elif' expr colcom objectPart COMMENT?)*
            ('else' colcom objectPart COMMENT?)?
objectBranch = 'of' exprList colcom objectPart
objectBranches = objectBranch (IND{=} objectBranch)*
                      (IND{=} 'elif' expr colcom objectPart)*
                      (IND{=} 'else' colcom objectPart)?
objectCase = 'case' (declColonEquals / pragma)? ':'? COMMENT?
            (IND{>} objectBranches DED
            | IND{=} objectBranches)
objectPart = IND{>} objectPart^+IND{=} DED
           / objectWhen / objectCase / 'nil' / 'discard' / declColonEquals
objectDecl = 'object' ('of' typeDesc)? COMMENT? objectPart
conceptParam = ('var' | 'out' | 'ptr' | 'ref' | 'static' | 'type')? symbol
conceptDecl = 'concept' conceptParam ^* ',' (pragma)? ('of' typeDesc ^* ',')?
              &IND{>} stmt
typeDef = identVisDot genericParamList? pragma '=' optInd typeDefValue
            indAndComment?
varTupleLhs = '(' optInd (identWithPragma / varTupleLhs) ^+ comma optPar ')' (':' optInd typeDescExpr)?
varTuple = varTupleLhs '=' optInd expr
colonBody = colcom stmt postExprBlocks?
variable = (varTuple / identColonEquals) colonBody? indAndComment
constant = (varTuple / identWithPragma) (colon typeDesc)? '=' optInd expr indAndComment
bindStmt = 'bind' optInd qualifiedIdent ^+ comma
mixinStmt = 'mixin' optInd qualifiedIdent ^+ comma
pragmaStmt = pragma (':' COMMENT? stmt)?
simpleStmt = ((returnStmt | raiseStmt | yieldStmt | discardStmt | breakStmt
           | continueStmt | pragmaStmt | importStmt | exportStmt | fromStmt
           | includeStmt | commentStmt) / exprStmt) COMMENT?
complexOrSimpleStmt = (ifStmt | whenStmt | whileStmt
                    | tryStmt | forStmt
                    | blockStmt | staticStmt | deferStmt | asmStmt
                    | 'proc' routine
                    | 'method' routine
                    | 'func' routine
                    | 'iterator' routine
                    | 'macro' routine
                    | 'template' routine
                    | 'converter' routine
                    | 'type' section(typeDef)
                    | 'const' section(constant)
                    | ('let' | 'var' | 'using') section(variable)
                    | bindStmt | mixinStmt)
                    / simpleStmt
stmt = (IND{>} complexOrSimpleStmt^+(IND{=} / ';') DED)
     / simpleStmt ^+ ';'

여기서 module이 어떻게 여러 문장들로 이어지는지, 각 문장(complexOrSimpleStmt)이 또 어떤 세부 문장들로 갈라지는지를 따라가다 보면 Nim 문법 전체의 뼈대가 그려진답니다.

더 알아보기 (Learn more)