문법
문법
이 섹션은 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 ^/ 8은 12 ^/ (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.c는 a.?(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)
- Lexical Analysis — 들여쓰기와 토큰 단위 해석 규칙
- Nim Manual: Syntax — 원문 전체
- Nim Manual — 매뉴얼 첫 화면