02. Name Resolver
nn 개발기 2편
2024-09-11 a5e9153
파서와 간단한 코드젠(트랜스폼)을 만들고 그 다음에 만들어야겠다 생각한 건 타입 체커였다.
사실상의 메인 피쳐이고, 가장 구현하는 데 시간이 오래 걸렸다.
src/resolver/types.ts#3-28
export interface FileScope {
path: string;
declarations: DeclarationScope[];
}
export interface DeclarationScope {
file: FileScope;
declaration: string;
sizes: Size[];
values: Value[];
}
export interface Size {
scope: DeclarationScope;
ident: string;
nodes: Set<Node>;
}
export interface Value {
scope: DeclarationScope;
ident: string;
nodes: Set<Node>;
}
Name Resolver에 사용할 타입들의 정의이다.
- 파일 단위의 Scope를 기록할 FileScope
- 함수 정의 내에서의 Scope를 기록할 DeclarationScope
- 이름에 따라 AST 노드들을 묶는 Size, Value
구현은 이 타입 정의에 따라 내부 값들을 채워넣는 게 전부였다.
src/resolver/index.ts#37-86
for (const decl of sourceCode) {
const declScope: DeclarationScope = {
file: fileScope,
declaration: decl.name.value,
sizes: [],
values: []
}
decl.sizeDeclList.decls
.forEach(ident => {
const sizeScope = toSize(declScope, ident);
declScope.sizes.push(sizeScope);
});
decl.argumentList.args
.forEach(arg => {
const valueScope = toValue(declScope, arg.ident);
declScope.values.push(valueScope);
});
const callExpressions = travel(decl.exprs, isCallExpression);
const identExprs = travel(decl.exprs, isIdentifierExpression);
identExprs
.forEach(identExpr => {
const value = findValue(declScope, identExpr.ident.value);
if (value) {
value.nodes.add(identExpr.ident);
} else {
const newValue = toValue(declScope, identExpr.ident);
declScope.values.push(newValue);
}
});
callExpressions
.flatMap(sizeDeclList => sizeDeclList.sizes)
.filter(ident => !!ident)
.filter(ident => typeof ident !== "number")
.forEach(ident => {
const size = findSize(declScope, ident.value);
if (size) {
size.nodes.add(ident);
} else {
const newSize = toSize(declScope, ident);
declScope.sizes.push(newSize);
}
});
fileScope.declarations.push(declScope);
}
처음 구현했던 name revoler 코드이다.
위 함수를 구현하고 느낀 건데, 생각했던 것 만큼 구현 과정이 어렵진 않았다.
- 일단 필요한 값들과 타입을 잘 정의한 후,
- travel 함수를 잘 이용하여 목적 노드들을 뽑아오고,
- 구한 노드에서 적절한 처리를 하여 필요한 값들을 얻어내면 완성
이라는 큰 틀이 잡히기 시작했다.
Monorepo
그 외 큰 변경점이라면, yarn monorepo를 활용해 모듈을 크게 세 개로 분리했다.
파일 구조
- root
- packages
- nn-language (파싱, 기본적인 처리)
- nn-language-server (언어 서버)
- nn-type-checker (이름, 타입 체킹)
- packages
대략 이런 형태인데, 이 중 언어 서버 구현에 관한 내용을 다음 글에서 풀어볼까 한다.